tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ main widgets/panes/examples/panes_demo.rs Rust · 319 lines · 8.8 KB History Who touched what raw

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! Demo de `llimphi-widget-panes` — "tmux de componentes tawasuyu".
2//!
3//! Dos tipos de panel heterogéneos (Contador y Notas) conviviendo en un
4//! mismo árbol BSP que se parte horizontal/vertical, se cierra, se enfoca
5//! (click) y se redimensiona (arrastrando los divisores). Prueba de punta
6//! a punta de que componentes distintos se montan en un layout
7//! intercambiable con splits resizables.
8//!
9//! Correr:  `cargo run -p llimphi-widget-panes --example panes_demo --release`
10
11use std::collections::HashMap;
12
13use llimphi_ui::llimphi_layout::taffy::{
14    prelude::{length, percent, FlexDirection, Size, Style},
15    Rect,
16};
17use llimphi_ui::{App, Handle, View};
18use llimphi_theme::Theme;
19use llimphi_widget_panes::{panes_view, Axis, Layout, PaneId, PanesPalette, Side};
20
21struct Demo;
22
23#[derive(Clone)]
24enum Msg {
25    Focus(PaneId),
26    Split(Axis),
27    Close,
28    Resize(Vec<Side>, f32),
29    Inc(PaneId),
30    Dec(PaneId),
31    AddNote(PaneId),
32}
33
34enum Kind {
35    Counter(i64),
36    Notes(Vec<String>),
37}
38
39struct Pane {
40    title: String,
41    kind: Kind,
42}
43
44struct Model {
45    layout: Layout,
46    panes: HashMap<PaneId, Pane>,
47    focused: PaneId,
48    next_id: PaneId,
49    theme: Theme,
50}
51
52impl App for Demo {
53    type Model = Model;
54    type Msg = Msg;
55
56    fn title() -> &'static str {
57        "panes — tmux de componentes tawasuyu"
58    }
59
60    fn init(_: &Handle<Msg>) -> Model {
61        let mut panes = HashMap::new();
62        panes.insert(
63            1,
64            Pane {
65                title: "Contador".into(),
66                kind: Kind::Counter(0),
67            },
68        );
69        panes.insert(
70            2,
71            Pane {
72                title: "Notas".into(),
73                kind: Kind::Notes(vec!["arrastra el divisor del medio →".into()]),
74            },
75        );
76        let mut layout = Layout::single(1);
77        layout.split(1, 2, Axis::Horizontal);
78        Model {
79            layout,
80            panes,
81            focused: 1,
82            next_id: 3,
83            theme: Theme::dark(),
84        }
85    }
86
87    fn update(mut model: Model, msg: Msg, _: &Handle<Msg>) -> Model {
88        match msg {
89            Msg::Focus(id) => model.focused = id,
90            Msg::Split(axis) => {
91                let id = model.next_id;
92                model.next_id += 1;
93                let kind = if id % 2 == 0 {
94                    Kind::Counter(0)
95                } else {
96                    Kind::Notes(vec![])
97                };
98                let title = match &kind {
99                    Kind::Counter(_) => "Contador".to_string(),
100                    Kind::Notes(_) => "Notas".to_string(),
101                };
102                model.panes.insert(id, Pane { title, kind });
103                model.layout.split(model.focused, id, axis);
104                model.focused = id;
105            }
106            Msg::Close => {
107                if model.layout.count() > 1 {
108                    let target = model.focused;
109                    let (nl, removed) = model.layout.clone().without(target);
110                    if removed {
111                        model.layout = nl;
112                        model.panes.remove(&target);
113                        model.focused = model.layout.first_leaf();
114                    }
115                }
116            }
117            Msg::Resize(path, d) => model.layout.resize(&path, d),
118            Msg::Inc(id) => {
119                if let Some(Pane {
120                    kind: Kind::Counter(n),
121                    ..
122                }) = model.panes.get_mut(&id)
123                {
124                    *n += 1;
125                }
126            }
127            Msg::Dec(id) => {
128                if let Some(Pane {
129                    kind: Kind::Counter(n),
130                    ..
131                }) = model.panes.get_mut(&id)
132                {
133                    *n -= 1;
134                }
135            }
136            Msg::AddNote(id) => {
137                if let Some(Pane {
138                    kind: Kind::Notes(v),
139                    ..
140                }) = model.panes.get_mut(&id)
141                {
142                    let n = v.len() + 1;
143                    v.push(format!("nota #{n}"));
144                }
145            }
146        }
147        model
148    }
149
150    fn view(model: &Model) -> View<Msg> {
151        let t = &model.theme;
152        let toolbar = View::new(Style {
153            flex_direction: FlexDirection::Row,
154            gap: Size {
155                width: length(8.0),
156                height: length(8.0),
157            },
158            padding: uniform(8.0),
159            flex_shrink: 0.0,
160            ..Default::default()
161        })
162        .fill(t.bg_panel)
163        .children(vec![
164            button("Split →", Msg::Split(Axis::Horizontal), t),
165            button("Split ↓", Msg::Split(Axis::Vertical), t),
166            button("Cerrar", Msg::Close, t),
167            View::new(Style {
168                flex_grow: 1.0,
169                ..Default::default()
170            }),
171            label(
172                format!("foco #{}  ·  {} paneles", model.focused, model.layout.count()),
173                13.0,
174                t.fg_muted,
175            ),
176        ]);
177
178        let palette = PanesPalette::from_theme(t);
179        let panes = &model.panes;
180        let theme = t;
181        let area = panes_view(
182            &model.layout,
183            model.focused,
184            move |id| render_pane(panes, theme, id),
185            |path, phase, d| {
186                let _ = phase;
187                Some(Msg::Resize(path, d))
188            },
189            Msg::Focus,
190            &palette,
191        );
192
193        let area_wrap = View::new(Style {
194            flex_grow: 1.0,
195            size: Size {
196                width: percent(1.0),
197                height: percent(1.0),
198            },
199            min_size: Size {
200                width: length(0.0),
201                height: length(0.0),
202            },
203            ..Default::default()
204        })
205        .children(vec![area]);
206
207        View::new(Style {
208            flex_direction: FlexDirection::Column,
209            size: Size {
210                width: percent(1.0),
211                height: percent(1.0),
212            },
213            ..Default::default()
214        })
215        .fill(t.bg_app)
216        .children(vec![toolbar, area_wrap])
217    }
218}
219
220fn render_pane(panes: &HashMap<PaneId, Pane>, t: &Theme, id: PaneId) -> View<Msg> {
221    let Some(pane) = panes.get(&id) else {
222        return label("(panel vacío)".to_string(), 14.0, t.fg_muted);
223    };
224
225    let header = label(format!("{}  #{id}", pane.title), 13.0, t.fg_text);
226
227    let body = match &pane.kind {
228        Kind::Counter(n) => View::new(Style {
229            flex_direction: FlexDirection::Column,
230            gap: Size {
231                width: length(8.0),
232                height: length(8.0),
233            },
234            ..Default::default()
235        })
236        .children(vec![
237            label(format!("{n}"), 44.0, t.accent),
238            View::new(Style {
239                flex_direction: FlexDirection::Row,
240                gap: Size {
241                    width: length(8.0),
242                    height: length(8.0),
243                },
244                ..Default::default()
245            })
246            .children(vec![
247                button("−", Msg::Dec(id), t),
248                button("+", Msg::Inc(id), t),
249            ]),
250        ]),
251        Kind::Notes(v) => {
252            let mut lines: Vec<View<Msg>> = v
253                .iter()
254                .map(|s| label(format!("• {s}"), 14.0, t.fg_text))
255                .collect();
256            lines.push(button("+ nota", Msg::AddNote(id), t));
257            View::new(Style {
258                flex_direction: FlexDirection::Column,
259                gap: Size {
260                    width: length(6.0),
261                    height: length(6.0),
262                },
263                ..Default::default()
264            })
265            .children(lines)
266        }
267    };
268
269    View::new(Style {
270        flex_direction: FlexDirection::Column,
271        gap: Size {
272            width: length(10.0),
273            height: length(10.0),
274        },
275        padding: uniform(12.0),
276        flex_grow: 1.0,
277        ..Default::default()
278    })
279    .children(vec![header, body])
280}
281
282fn button(text: &str, msg: Msg, t: &Theme) -> View<Msg> {
283    View::new(Style {
284        padding: Rect {
285            left: length(12.0),
286            right: length(12.0),
287            top: length(6.0),
288            bottom: length(6.0),
289        },
290        flex_shrink: 0.0,
291        ..Default::default()
292    })
293    .fill(t.bg_button)
294    .hover_fill(t.bg_button_hover)
295    .radius(6.0)
296    .on_click(msg)
297    .children(vec![label(text.to_string(), 14.0, t.fg_text)])
298}
299
300fn label(
301    text: String,
302    size: f32,
303    color: llimphi_ui::llimphi_raster::peniko::Color,
304) -> View<Msg> {
305    View::new(Style::default()).text(text, size, color)
306}
307
308fn uniform(px: f32) -> Rect<llimphi_ui::llimphi_layout::taffy::prelude::LengthPercentage> {
309    Rect {
310        left: length(px),
311        right: length(px),
312        top: length(px),
313        bottom: length(px),
314    }
315}
316
317fn main() {
318    llimphi_ui::run::<Demo>();
319}