tawasuyu

tawasuyu/llimphi

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

Rust 124Markdown 82TOML 78Shell 12149 Sutichaykuna

⎇ main widgets/tabs/examples/tabs_demo.rs Rust · 188 siq'ikuna · 5.6 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

1//! Showcase de `llimphi-widget-tabs` con las 3 features nuevas:
2//! **cerrar** (× por tab), **reordenar** (arrastrar un tab sobre otro) y
3//! **overflow** (9 tabs que no caben → la tira scrollea con la rueda).
4//!
5//! Corre con: `cargo run -p llimphi-widget-tabs --example tabs_demo --release`.
6//! - Click en un tab → lo activa.
7//! - Click en el × → cierra ese tab.
8//! - Arrastra un tab sobre otro → los reordena.
9//! - Rueda sobre la tira → scroll horizontal cuando desbordan.
10
11use std::sync::Arc;
12
13use llimphi_ui::llimphi_layout::taffy::{
14    prelude::{length, percent, Size, Style},
15    AlignItems, Rect,
16};
17use llimphi_ui::llimphi_raster::peniko::Color;
18use llimphi_ui::llimphi_text::Alignment;
19use llimphi_ui::{App, Handle, View};
20use llimphi_widget_tabs::{
21    clamp_strip_offset, reorder, strip_width, tabs_view, TabsPalette, TabsSpec,
22};
23
24/// Ancho fijo de cada tab; con 9 tabs (9·150 + gaps ≈ 1366) desborda la
25/// ventana de 900 → se ejercita el overflow.
26const TAB_W: f32 = 150.0;
27/// Ancho aproximado de la barra (≈ ancho de ventana) para clampear el scroll.
28const BAR_W: f32 = 900.0;
29
30#[derive(Clone)]
31enum Msg {
32    SelectTab(usize),
33    CloseTab(usize),
34    ReorderTab(usize, usize),
35    ScrollStrip(f32),
36}
37
38struct Model {
39    tabs: Vec<String>,
40    active: usize,
41    strip_offset: f32,
42}
43
44struct Showcase;
45
46impl App for Showcase {
47    type Model = Model;
48    type Msg = Msg;
49
50    fn title() -> &'static str {
51        "llimphi · tabs showcase"
52    }
53
54    fn initial_size() -> (u32, u32) {
55        (900, 600)
56    }
57
58    fn init(_: &Handle<Msg>) -> Model {
59        Model {
60            tabs: vec![
61                "General".into(),
62                "Avanzado".into(),
63                "Logs".into(),
64                "Red".into(),
65                "Audio".into(),
66                "Video".into(),
67                "Atajos".into(),
68                "Plugins".into(),
69                "Acerca de".into(),
70            ],
71            active: 0,
72            strip_offset: 0.0,
73        }
74    }
75
76    fn update(model: Model, msg: Msg, _: &Handle<Msg>) -> Model {
77        let mut m = model;
78        match msg {
79            Msg::SelectTab(i) => m.active = i.min(m.tabs.len().saturating_sub(1)),
80            Msg::CloseTab(i) => {
81                if i < m.tabs.len() {
82                    m.tabs.remove(i);
83                    // Reajustar el activo tras el cierre.
84                    if m.active >= i && m.active > 0 {
85                        m.active -= 1;
86                    }
87                    m.active = m.active.min(m.tabs.len().saturating_sub(1));
88                }
89            }
90            Msg::ReorderTab(from, to) => {
91                let active_name = m.tabs.get(m.active).cloned();
92                reorder(&mut m.tabs, from, to);
93                // Mantener activo el mismo tab (por nombre) tras mover.
94                if let Some(name) = active_name {
95                    if let Some(pos) = m.tabs.iter().position(|t| *t == name) {
96                        m.active = pos;
97                    }
98                }
99            }
100            Msg::ScrollStrip(delta) => {
101                let content = strip_width(&m.tabs, Some(TAB_W), true);
102                m.strip_offset = clamp_strip_offset(m.strip_offset + delta, content, BAR_W);
103            }
104        }
105        m
106    }
107
108    fn view(model: &Model) -> View<Msg> {
109        let title = model
110            .tabs
111            .get(model.active)
112            .cloned()
113            .unwrap_or_else(|| "—".into());
114        let body = content_pane(
115            &title,
116            "Click cambia de tab · el × cierra · arrastra un tab sobre otro para\n\
117             reordenar · la rueda sobre la tira scrollea cuando los tabs desbordan.",
118            Color::from_rgba8(220, 230, 245, 255),
119        );
120
121        tabs_view(TabsSpec {
122            labels: model.tabs.clone(),
123            active: model.active,
124            on_select: Msg::SelectTab,
125            content: body,
126            tab_height: 36.0,
127            palette: TabsPalette::default(),
128            tab_width: Some(TAB_W),
129            on_close: Some(Arc::new(Msg::CloseTab)),
130            on_reorder: Some(Arc::new(|from, to| Msg::ReorderTab(from, to))),
131            strip_offset: model.strip_offset,
132            on_strip_scroll: Some(Arc::new(Msg::ScrollStrip)),
133        })
134    }
135}
136
137fn content_pane(title: &str, body: &str, fg: Color) -> View<Msg> {
138    let header = View::new(Style {
139        size: Size {
140            width: percent(1.0_f32),
141            height: length(36.0_f32),
142        },
143        padding: Rect {
144            left: length(20.0_f32),
145            right: length(20.0_f32),
146            top: length(8.0_f32),
147            bottom: length(0.0_f32),
148        },
149        align_items: Some(AlignItems::START),
150        ..Default::default()
151    })
152    .text_aligned(
153        format!("# {title}"),
154        18.0,
155        Color::from_rgba8(220, 230, 245, 255),
156        Alignment::Start,
157    );
158
159    let body_view = View::new(Style {
160        size: Size {
161            width: percent(1.0_f32),
162            height: percent(1.0_f32),
163        },
164        flex_grow: 1.0,
165        padding: Rect {
166            left: length(20.0_f32),
167            right: length(20.0_f32),
168            top: length(0.0_f32),
169            bottom: length(20.0_f32),
170        },
171        ..Default::default()
172    })
173    .text_aligned(body.to_string(), 13.0, fg, Alignment::Start);
174
175    View::new(Style {
176        flex_direction: llimphi_ui::llimphi_layout::taffy::FlexDirection::Column,
177        size: Size {
178            width: percent(1.0_f32),
179            height: percent(1.0_f32),
180        },
181        ..Default::default()
182    })
183    .children(vec![header, body_view])
184}
185
186fn main() {
187    llimphi_ui::run::<Showcase>();
188}