tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ 92dd0e5bfb2ca4456006da99437f4100e156d856 widgets/splitter/src/lib.rs Rust · 186 líneas · 5.8 KB Historia Quién tocó qué crudo

refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel ccab39f14 · Sergio ·

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}