tawasuyu

tawasuyu/brahman

Rust 332TOML 127JSON 17Markdown 154710 Definiciones

⎇ 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd crates/modules/ui_engine/widgets/splitter/src/lib.rs Rust · 362 líneas · 13.8 KB Historia Quién tocó qué crudo

chore: monorepo inicial con arje + minga + yahweh absorbidos 53dbdf0f1 · Sergio ·

1//! `yahweh_widget_splitter` — `SplitContainer` genérico.
2//!
3//! Aloja `n` hijos `AnyView` con flex weights individuales y un divisor
4//! arrastrable entre cada par adyacente. Dirección horizontal o vertical
5//! intercambiable. Emite [`SplitEvent::FlexChanged`] cuando un drag termina,
6//! para que el host (LayoutHost / DemoApp) persista los flex.
7//!
8//! El SplitContainer NO conoce a sus hijos: los recibe vía
9//! `set_children(Vec<ChildSlot>)`. Eso permite que el LayoutHost reuse las
10//! mismas instancias cuando el JSON cambia el `kind` del contenedor (Split
11//! → Tabs → Tiled) — los AnyView siguen vivos, solo cambia su contenedor.
12//!
13//! Drag: usamos el patrón canónico de gpui (ver `data_table.rs` ejemplo) —
14//! cada divider tiene un `canvas(prepaint, paint)` que en su paint registra
15//! handlers de `MouseDown / MouseMove / MouseUp` a nivel de window vía
16//! `window.on_mouse_event`. Esto garantiza que el drag continúa aunque el
17//! cursor salga del divider.
18
19use std::cell::RefCell;
20use std::rc::Rc;
21
22use gpui::{
23    App, Bounds, Context, EventEmitter, IntoElement, Length, MouseButton, MouseDownEvent,
24    MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, Window, canvas, div, prelude::*, px,
25};
26
27use yahweh_core::{LayoutDirection, NodeId};
28use yahweh_theme::Theme;
29pub use yahweh_widget_container_core::ChildSlot;
30
31#[derive(Clone, Debug)]
32pub enum SplitEvent {
33    /// Un drag actualizó los flex weights. Se emite UNA vez por movimiento
34    /// (cada frame durante un drag), con los IDs y flex finales de los dos
35    /// hijos adyacentes al divisor.
36    FlexChanged {
37        left_id: NodeId,
38        right_id: NodeId,
39        left_flex: f32,
40        right_flex: f32,
41    },
42    /// El drag terminó (mouseup). Útil para persistir batched.
43    DragEnd,
44}
45
46// =====================================================================
47// Widget
48// =====================================================================
49
50/// Estado interno del drag activo. `divider_index` apunta al espacio entre
51/// `children[i]` y `children[i+1]`. Los snapshots `flex_*_initial` y
52/// `start_pos_main` se capturan en MouseDown — durante MouseMove se
53/// recalcula el flex desde el delta.
54struct DragState {
55    divider_index: usize,
56    start_pos_main: Pixels,
57    flex_left_initial: f32,
58    flex_right_initial: f32,
59    /// Longitud total del SplitContainer en el eje principal al iniciar el
60    /// drag (capturada de `bounds`). Usada para convertir delta_px ↔
61    /// delta_flex preservando el sum total.
62    total_main_size: Pixels,
63    total_flex_initial: f32,
64}
65
66pub struct SplitContainer {
67    children: Vec<ChildSlot>,
68    direction: LayoutDirection,
69    drag: Option<DragState>,
70    /// Bounds del frame anterior. Capturados vía canvas absolute en cada
71    /// paint. Lo usamos al iniciar drag para resolver `total_main_size`.
72    bounds: Rc<RefCell<Option<Bounds<Pixels>>>>,
73}
74
75impl EventEmitter<SplitEvent> for SplitContainer {}
76
77impl SplitContainer {
78    pub fn new(direction: LayoutDirection, cx: &mut Context<Self>) -> Self {
79        cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
80        Self {
81            children: Vec::new(),
82            direction,
83            drag: None,
84            bounds: Rc::new(RefCell::new(None)),
85        }
86    }
87
88    pub fn set_children(&mut self, children: Vec<ChildSlot>, cx: &mut Context<Self>) {
89        self.children = children;
90        cx.notify();
91    }
92
93    pub fn set_direction(&mut self, direction: LayoutDirection, cx: &mut Context<Self>) {
94        if self.direction != direction {
95            self.direction = direction;
96            cx.notify();
97        }
98    }
99
100    pub fn direction(&self) -> LayoutDirection {
101        self.direction
102    }
103
104    pub fn children(&self) -> &[ChildSlot] {
105        &self.children
106    }
107
108    // -------- Drag handlers --------
109
110    fn start_drag(&mut self, divider_index: usize, position: Point<Pixels>) {
111        if divider_index >= self.children.len().saturating_sub(1) {
112            return;
113        }
114        let bounds = match *self.bounds.borrow() {
115            Some(b) => b,
116            None => return,
117        };
118        let raw_main = main_axis(self.direction, bounds.size.width, bounds.size.height);
119        // Restamos el espacio que ocupan los divisores — son fixed-size en el
120        // eje principal, no participan del flex. El "espacio disponible
121        // para flex" es lo que importa para convertir delta_px → delta_flex.
122        let dividers_total = px(DIVIDER_THICKNESS) * (self.children.len().saturating_sub(1) as f32);
123        let total_main = raw_main - dividers_total;
124        if total_main <= px(0.0) {
125            return;
126        }
127
128        let total_flex: f32 = self.children.iter().map(|c| c.flex.max(0.0)).sum();
129        let total_flex = total_flex.max(0.001);
130
131        let start_main = main_axis_pt(self.direction, position);
132
133        self.drag = Some(DragState {
134            divider_index,
135            start_pos_main: start_main,
136            flex_left_initial: self.children[divider_index].flex,
137            flex_right_initial: self.children[divider_index + 1].flex,
138            total_main_size: total_main,
139            total_flex_initial: total_flex,
140        });
141    }
142
143    fn continue_drag(&mut self, position: Point<Pixels>, cx: &mut Context<Self>) {
144        let Some(drag) = &self.drag else { return };
145        let drag_idx = drag.divider_index;
146        if drag_idx + 1 >= self.children.len() {
147            return;
148        }
149
150        let cur_main = main_axis_pt(self.direction, position);
151        let delta_px = cur_main - drag.start_pos_main;
152        // delta_flex = delta_px / total_main_size * total_flex_initial.
153        let total_main_f = f32::from(drag.total_main_size).max(1.0);
154        let delta_flex = (f32::from(delta_px) / total_main_f) * drag.total_flex_initial;
155
156        const MIN_FLEX: f32 = 0.05;
157        let new_left = (drag.flex_left_initial + delta_flex).max(MIN_FLEX);
158        let new_right = (drag.flex_right_initial - delta_flex).max(MIN_FLEX);
159
160        // Solo aplicamos si NINGUNO se aplastó al mínimo y se "comió" el
161        // delta — eso significa que el drag llegó al borde de un hijo.
162        let fits = (drag.flex_left_initial + delta_flex) >= MIN_FLEX
163            && (drag.flex_right_initial - delta_flex) >= MIN_FLEX;
164        if !fits {
165            // Recortamos: aplicamos los mínimos pero no propagamos delta más
166            // allá del límite. Resultado: el divisor "frena" en el borde.
167        }
168
169        self.children[drag_idx].flex = new_left;
170        self.children[drag_idx + 1].flex = new_right;
171
172        let left_id = self.children[drag_idx].id.clone();
173        let right_id = self.children[drag_idx + 1].id.clone();
174        cx.emit(SplitEvent::FlexChanged {
175            left_id,
176            right_id,
177            left_flex: new_left,
178            right_flex: new_right,
179        });
180        cx.notify();
181    }
182
183    fn end_drag(&mut self, cx: &mut Context<Self>) {
184        if self.drag.take().is_some() {
185            cx.emit(SplitEvent::DragEnd);
186            cx.notify();
187        }
188    }
189}
190
191// =====================================================================
192// Helpers de eje
193// =====================================================================
194
195fn main_axis(dir: LayoutDirection, w: Pixels, h: Pixels) -> Pixels {
196    match dir {
197        LayoutDirection::Horizontal => w,
198        _ => h,
199    }
200}
201
202fn main_axis_pt(dir: LayoutDirection, p: Point<Pixels>) -> Pixels {
203    match dir {
204        LayoutDirection::Horizontal => p.x,
205        _ => p.y,
206    }
207}
208
209// =====================================================================
210// Render
211// =====================================================================
212
213const DIVIDER_THICKNESS: f32 = 4.0;
214
215impl Render for SplitContainer {
216    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
217        let theme = Theme::global(cx).clone();
218        let direction = self.direction;
219        let entity = cx.entity();
220        let bounds_holder = self.bounds.clone();
221
222        let total_flex: f32 = self
223            .children
224            .iter()
225            .map(|c| c.flex.max(0.0))
226            .sum::<f32>()
227            .max(0.001);
228
229        // Root flex container.
230        let mut root = div().size_full().relative();
231        root = match direction {
232            LayoutDirection::Horizontal => root.flex().flex_row(),
233            _ => root.flex().flex_col(),
234        };
235
236        // Canvas absolute para capturar bounds del SplitContainer en cada
237        // frame. No participa del flex (absolute), no captura clicks
238        // (canvas sin id es no-interactivo).
239        root = root.child({
240            let bounds_holder = bounds_holder.clone();
241            canvas(
242                move |bounds, _w, _cx| {
243                    *bounds_holder.borrow_mut() = Some(bounds);
244                },
245                |_, _, _, _| {},
246            )
247            .absolute()
248            .size_full()
249        });
250
251        // Children + dividers entre cada par.
252        let n = self.children.len();
253        for (i, child) in self.children.iter().enumerate() {
254            let weight = (child.flex.max(0.0) / total_flex).max(0.001);
255
256            let mut item = div().relative();
257            // flex_grow fraccional — el helper `flex_grow()` solo setea 1.0,
258            // así que vamos directo al campo subyacente para repartir
259            // proporcionalmente según el `flex` de cada slot.
260            item.style().flex_grow = Some(weight);
261            item.style().flex_shrink = Some(1.0);
262
263            // CRUCIAL: el default de flexbox es `min-width: auto` (= min
264            // content size). Si no lo aplastamos a 0, taffy clamp-ea al
265            // tamaño mínimo del contenido (un TreeView con label largo, un
266            // uniform_list, etc.) y el divisor no puede pasar de ese punto
267            // — el cursor avanza pero el divisor se queda. Forzando min=0
268            // y overflow:hidden en el wrapper, el child puede shrink-arse a
269            // donde sea y el contenido se recorta.
270            item.style().min_size.width = Some(Length::Definite(px(0.0).into()));
271            item.style().min_size.height = Some(Length::Definite(px(0.0).into()));
272
273            // Eje cruzado: full. Eje principal: lo decide flex.
274            let item = match direction {
275                LayoutDirection::Horizontal => item.h_full(),
276                _ => item.w_full(),
277            }
278            .overflow_hidden()
279            .child(child.view.clone());
280
281            root = root.child(item);
282
283            // Divisor entre i e i+1 (no después del último).
284            if i + 1 < n {
285                let divider_idx = i;
286                let entity_for_canvas = entity.clone();
287
288                let mut divider = div();
289                let divider_bg = if self.drag.as_ref().map(|d| d.divider_index) == Some(divider_idx)
290                {
291                    theme.accent_strong
292                } else {
293                    theme.border_strong
294                };
295                divider = divider.bg(divider_bg).hover(|s| s.bg(theme.accent));
296
297                divider = match direction {
298                    LayoutDirection::Horizontal => divider
299                        .w(px(DIVIDER_THICKNESS))
300                        .h_full()
301                        .cursor_ew_resize(),
302                    _ => divider
303                        .w_full()
304                        .h(px(DIVIDER_THICKNESS))
305                        .cursor_ns_resize(),
306                };
307
308                // Canvas con handlers de drag a nivel de window.
309                let divider = divider.child(
310                    canvas(
311                        |_, _, _| (),
312                        move |canvas_bounds: Bounds<Pixels>, _, window, _| {
313                            // MouseDown sobre el divisor → start_drag.
314                            window.on_mouse_event({
315                                let entity = entity_for_canvas.clone();
316                                move |ev: &MouseDownEvent, _, _w: &mut Window, cx: &mut App| {
317                                    if ev.button != MouseButton::Left {
318                                        return;
319                                    }
320                                    if !canvas_bounds.contains(&ev.position) {
321                                        return;
322                                    }
323                                    entity.update(cx, |this, _| {
324                                        this.start_drag(divider_idx, ev.position);
325                                    });
326                                }
327                            });
328
329                            // MouseMove anywhere → continue_drag si hay drag.
330                            window.on_mouse_event({
331                                let entity = entity_for_canvas.clone();
332                                move |ev: &MouseMoveEvent, _, _w: &mut Window, cx: &mut App| {
333                                    if !ev.dragging() {
334                                        return;
335                                    }
336                                    entity.update(cx, |this, cx| {
337                                        if this.drag.is_some() {
338                                            this.continue_drag(ev.position, cx);
339                                        }
340                                    });
341                                }
342                            });
343
344                            // MouseUp anywhere → end_drag.
345                            window.on_mouse_event({
346                                let entity = entity_for_canvas.clone();
347                                move |_: &MouseUpEvent, _, _w: &mut Window, cx: &mut App| {
348                                    entity.update(cx, |this, cx| this.end_drag(cx));
349                                }
350                            });
351                        },
352                    )
353                    .size_full(),
354                );
355
356                root = root.child(divider);
357            }
358        }
359
360        root
361    }
362}