tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
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}