tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 92dd0e5bfb2ca4456006da99437f4100e156d856
widgets/splitter/src/lib.rs
Historia
Quién tocó qué
crudo
refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
1//! `llimphi-widget-splitter` — split container con divisor draggable.
2//!
3//! Análogo Llimphi al `nahual-widget-splitter` GPUI: dos panes con un
4//! divisor entre medio que el usuario arrastra para reasignar el tamaño.
5//! El widget no mantiene estado: el caller acumula el tamaño de un pane
6//! en su `Model` y le pasa el valor actual + un handler `Fn(DragPhase,
7//! f32) -> Option<Msg>` que materializa el delta en un Msg de update.
8//!
9//! Uso típico (dos panes, izquierdo fijo y derecho flex):
10//!
11//! ```ignore
12//! splitter_two(
13//! Direction::Row,
14//! left_view,
15//! PaneSize::Fixed(model.left_size),
16//! right_view,
17//! PaneSize::Flex,
18//! |phase, dx| match phase {
19//! DragPhase::Move => Some(Msg::ResizeLeft(dx)),
20//! DragPhase::End => Some(Msg::PersistLayout),
21//! },
22//! &SplitterPalette::default(),
23//! )
24//! ```
25
26#![forbid(unsafe_code)]
27
28use std::sync::Arc;
29
30use llimphi_ui::llimphi_layout::taffy::{
31 prelude::{length, percent, FlexDirection, Size, Style},
32 Rect,
33};
34use llimphi_ui::llimphi_raster::peniko::Color;
35use llimphi_ui::{DragPhase, View};
36
37/// Dirección del split. `Row` apila los panes horizontalmente
38/// (divisor vertical, drag horizontal); `Column` los apila verticalmente.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Direction {
41 Row,
42 Column,
43}
44
45/// Tamaño de un pane sobre el eje principal del split.
46#[derive(Debug, Clone, Copy)]
47pub enum PaneSize {
48 /// Ancho/alto fijo en pixels. El otro pane se ajusta con `flex_grow`.
49 Fixed(f32),
50 /// Toma todo el espacio sobrante (`flex_grow = 1`).
51 Flex,
52}
53
54/// Paleta del divisor. Cambia de color al hover para señalar
55/// "agarrame y arrastrá".
56#[derive(Debug, Clone, Copy)]
57pub struct SplitterPalette {
58 pub divider: Color,
59 pub divider_hover: Color,
60 pub thickness: f32,
61}
62
63impl Default for SplitterPalette {
64 fn default() -> Self {
65 Self::from_theme(&llimphi_theme::Theme::dark())
66 }
67}
68
69impl SplitterPalette {
70 /// Construye la paleta desde un `Theme` semántico.
71 pub fn from_theme(t: &llimphi_theme::Theme) -> Self {
72 Self {
73 divider: t.border,
74 divider_hover: t.accent,
75 thickness: 6.0,
76 }
77 }
78}
79
80/// Split de dos panes con divisor draggable entre medio. `on_resize`
81/// se invoca con el delta del eje principal (positivo → divisor se
82/// mueve a la derecha/abajo).
83pub fn splitter_two<Msg, F>(
84 direction: Direction,
85 a: View<Msg>,
86 a_size: PaneSize,
87 b: View<Msg>,
88 b_size: PaneSize,
89 on_resize: F,
90 palette: &SplitterPalette,
91) -> View<Msg>
92where
93 Msg: Clone + Send + Sync + 'static,
94 F: Fn(DragPhase, f32) -> Option<Msg> + Send + Sync + 'static,
95{
96 let flex_dir = match direction {
97 Direction::Row => FlexDirection::Row,
98 Direction::Column => FlexDirection::Column,
99 };
100
101 // El divisor sólo necesita Msg en el eje principal — escondemos el
102 // otro detrás del closure.
103 let on_resize = Arc::new(on_resize);
104 let cb_dir = direction;
105 let cb = on_resize.clone();
106 let divider = divider_view::<Msg>(direction, palette, move |phase, dx, dy| {
107 let main = match cb_dir {
108 Direction::Row => dx,
109 Direction::Column => dy,
110 };
111 (cb)(phase, main)
112 });
113
114 let pane_a = wrap_pane(a, direction, a_size);
115 let pane_b = wrap_pane(b, direction, b_size);
116
117 View::new(Style {
118 flex_direction: flex_dir,
119 size: Size {
120 width: percent(1.0_f32),
121 height: percent(1.0_f32),
122 },
123 ..Default::default()
124 })
125 .children(vec![pane_a, divider, pane_b])
126}
127
128fn wrap_pane<Msg>(view: View<Msg>, direction: Direction, size: PaneSize) -> View<Msg> {
129 // El panel Flex usa flex-basis 0 en el eje principal (no `auto`): así su
130 // tamaño es la porción de espacio libre que le toca por `flex_grow`, en
131 // vez del tamaño de su contenido. Con `auto` un hijo que desborda (p.ej.
132 // una grilla con flex-wrap o contenido ancho) volvía el panel
133 // content-sized y rompía el wrap / desbordaba horizontalmente.
134 let (width, height, flex_grow) = match (direction, size) {
135 (Direction::Row, PaneSize::Fixed(px)) => (length(px), percent(1.0_f32), 0.0),
136 (Direction::Row, PaneSize::Flex) => (length(0.0_f32), percent(1.0_f32), 1.0),
137 (Direction::Column, PaneSize::Fixed(px)) => (percent(1.0_f32), length(px), 0.0),
138 (Direction::Column, PaneSize::Flex) => (percent(1.0_f32), length(0.0_f32), 1.0),
139 };
140 View::new(Style {
141 size: Size { width, height },
142 flex_grow,
143 flex_shrink: 0.0,
144 min_size: Size {
145 width: length(0.0_f32),
146 height: length(0.0_f32),
147 },
148 ..Default::default()
149 })
150 .children(vec![view])
151}
152
153fn divider_view<Msg>(
154 direction: Direction,
155 palette: &SplitterPalette,
156 handler: impl Fn(DragPhase, f32, f32) -> Option<Msg> + Send + Sync + 'static,
157) -> View<Msg>
158where
159 Msg: Clone + Send + Sync + 'static,
160{
161 let (width, height) = match direction {
162 Direction::Row => (length(palette.thickness), percent(1.0_f32)),
163 Direction::Column => (percent(1.0_f32), length(palette.thickness)),
164 };
165 // Divisor vertical (Row, separa izq/der) → resize este-oeste; divisor
166 // horizontal (Column, separa arriba/abajo) → resize norte-sur.
167 let cursor = match direction {
168 Direction::Row => llimphi_ui::Cursor::ColResize,
169 Direction::Column => llimphi_ui::Cursor::RowResize,
170 };
171 View::new(Style {
172 size: Size { width, height },
173 flex_shrink: 0.0,
174 padding: Rect {
175 left: length(0.0_f32),
176 right: length(0.0_f32),
177 top: length(0.0_f32),
178 bottom: length(0.0_f32),
179 },
180 ..Default::default()
181 })
182 .fill(palette.divider)
183 .hover_fill(palette.divider_hover)
184 .cursor(cursor)
185 .draggable(handler)
186}