tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! Demo del chasis `llimphi-workspace`.
2//!
3//! Mismo resultado que `panes_demo` pero la app ya no reimplementa la
4//! máquina de estados: guarda un `Workspace` + un mapa de paneles, y deja
5//! que el chasis maneje split/cerrar/foco/resize y el chrome. Esto es el
6//! molde que después adopta cada app de tawasuyu.
7//!
8//! Correr: `cargo run -p llimphi-workspace --example workspace_demo --release`
9
10use std::collections::HashMap;
11
12use llimphi_ui::llimphi_layout::taffy::{
13 prelude::{length, FlexDirection, Size, Style},
14 Rect,
15};
16use llimphi_ui::{App, Handle, View};
17use llimphi_theme::Theme;
18use llimphi_workspace::{workspace_view, Axis, PaneId, Workspace, WorkspacePalette, WsEffect, WsMsg};
19
20struct Demo;
21
22#[derive(Clone)]
23enum Msg {
24 Ws(WsMsg),
25 Panel(PaneId, PanelMsg),
26}
27
28#[derive(Clone)]
29enum PanelMsg {
30 Inc,
31 Dec,
32 AddNote,
33}
34
35enum Kind {
36 Counter(i64),
37 Notes(Vec<String>),
38}
39
40struct Model {
41 ws: Workspace,
42 panes: HashMap<PaneId, Kind>,
43 theme: Theme,
44}
45
46impl App for Demo {
47 type Model = Model;
48 type Msg = Msg;
49
50 fn title() -> &'static str {
51 "workspace — chasis tmux de tawasuyu"
52 }
53
54 fn init(_: &Handle<Msg>) -> Model {
55 let mut ws = Workspace::new(); // panel 0
56 let mut panes = HashMap::new();
57 panes.insert(0, Kind::Counter(0));
58 let id = ws.split(Axis::Horizontal);
59 panes.insert(id, Kind::Notes(vec!["arrastra el divisor del medio →".into()]));
60 ws.focus(0);
61 Model {
62 ws,
63 panes,
64 theme: Theme::dark(),
65 }
66 }
67
68 fn update(mut model: Model, msg: Msg, _: &Handle<Msg>) -> Model {
69 match msg {
70 Msg::Ws(m) => match model.ws.apply(m) {
71 WsEffect::Created(id) => {
72 // Alternamos tipo para ilustrar paneles heterogéneos.
73 let kind = if id % 2 == 0 {
74 Kind::Counter(0)
75 } else {
76 Kind::Notes(vec![])
77 };
78 model.panes.insert(id, kind);
79 }
80 WsEffect::Closed(id) => {
81 model.panes.remove(&id);
82 }
83 WsEffect::None => {}
84 },
85 Msg::Panel(id, pm) => {
86 if let Some(kind) = model.panes.get_mut(&id) {
87 match (kind, pm) {
88 (Kind::Counter(n), PanelMsg::Inc) => *n += 1,
89 (Kind::Counter(n), PanelMsg::Dec) => *n -= 1,
90 (Kind::Notes(v), PanelMsg::AddNote) => {
91 let n = v.len() + 1;
92 v.push(format!("nota #{n}"));
93 }
94 _ => {}
95 }
96 }
97 }
98 }
99 model
100 }
101
102 fn view(model: &Model) -> View<Msg> {
103 let palette = WorkspacePalette::from_theme(&model.theme);
104 let panes = &model.panes;
105 let theme = &model.theme;
106 workspace_view(
107 &model.ws,
108 &palette,
109 move |id| render_pane(panes, theme, id),
110 Msg::Ws,
111 )
112 }
113}
114
115fn render_pane(panes: &HashMap<PaneId, Kind>, t: &Theme, id: PaneId) -> View<Msg> {
116 let Some(kind) = panes.get(&id) else {
117 return label("(vacío)".to_string(), 14.0, t.fg_muted);
118 };
119 let body = match kind {
120 Kind::Counter(n) => col(
121 8.0,
122 vec![
123 label(format!("{n}"), 44.0, t.accent),
124 row(
125 8.0,
126 vec![
127 button("−", Msg::Panel(id, PanelMsg::Dec), t),
128 button("+", Msg::Panel(id, PanelMsg::Inc), t),
129 ],
130 ),
131 ],
132 ),
133 Kind::Notes(v) => {
134 let mut lines: Vec<View<Msg>> = v
135 .iter()
136 .map(|s| label(format!("• {s}"), 14.0, t.fg_text))
137 .collect();
138 lines.push(button("+ nota", Msg::Panel(id, PanelMsg::AddNote), t));
139 col(6.0, lines)
140 }
141 };
142 View::new(Style {
143 flex_direction: FlexDirection::Column,
144 gap: Size {
145 width: length(10.0),
146 height: length(10.0),
147 },
148 padding: uniform(12.0),
149 flex_grow: 1.0,
150 ..Default::default()
151 })
152 .children(vec![label(format!("panel #{id}"), 13.0, t.fg_muted), body])
153}
154
155fn button(text: &str, msg: Msg, t: &Theme) -> View<Msg> {
156 View::new(Style {
157 padding: Rect {
158 left: length(12.0),
159 right: length(12.0),
160 top: length(6.0),
161 bottom: length(6.0),
162 },
163 flex_shrink: 0.0,
164 ..Default::default()
165 })
166 .fill(t.bg_button)
167 .hover_fill(t.bg_button_hover)
168 .radius(6.0)
169 .on_click(msg)
170 .children(vec![label(text.to_string(), 14.0, t.fg_text)])
171}
172
173fn col(gap: f32, children: Vec<View<Msg>>) -> View<Msg> {
174 View::new(Style {
175 flex_direction: FlexDirection::Column,
176 gap: Size {
177 width: length(gap),
178 height: length(gap),
179 },
180 ..Default::default()
181 })
182 .children(children)
183}
184
185fn row(gap: f32, children: Vec<View<Msg>>) -> View<Msg> {
186 View::new(Style {
187 flex_direction: FlexDirection::Row,
188 gap: Size {
189 width: length(gap),
190 height: length(gap),
191 },
192 ..Default::default()
193 })
194 .children(children)
195}
196
197fn label(text: String, size: f32, color: llimphi_ui::llimphi_raster::peniko::Color) -> View<Msg> {
198 View::new(Style::default()).text(text, size, color)
199}
200
201fn uniform(px: f32) -> Rect<llimphi_ui::llimphi_layout::taffy::prelude::LengthPercentage> {
202 Rect {
203 left: length(px),
204 right: length(px),
205 top: length(px),
206 bottom: length(px),
207 }
208}
209
210fn main() {
211 llimphi_ui::run::<Demo>();
212}