tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos
1use super::*;
2
3pub(crate) struct Studio;
4
5impl App for Studio {
6 type Model = Model;
7 type Msg = Msg;
8
9 fn title() -> &'static str {
10 "llimphi-voxel-studio — creador de mundos"
11 }
12 fn initial_size() -> (u32, u32) {
13 (1180, 760)
14 }
15
16 fn init(handle: &Handle<Msg>) -> Model {
17 handle.spawn_periodic(Duration::from_millis(33), || Msg::Tick);
18 demo_model()
19 }
20
21 fn on_wheel(_m: &Model, delta: WheelDelta, _c: (f32, f32), _mods: Modifiers) -> Option<Msg> {
22 Some(Msg::Zoom(delta.y))
23 }
24
25 fn on_key(model: &Model, ev: &KeyEvent) -> Option<Msg> {
26 // En modo JUEGO el teclado maneja el movimiento, no los campos.
27 if model.jugando {
28 return juego_on_key(ev);
29 }
30 // Buscador de un sidebar unificado con foco: las teclas van al filtro
31 // (Esc/Enter lo sueltan; Backspace borra; el texto tipeado se inserta).
32 if model.sidebar_left.search_focused {
33 if ev.state != KeyState::Pressed {
34 return None;
35 }
36 return sidebar_search_key(ev, &model.sidebar_left.search, Msg::SidebarLeft);
37 }
38 if model.sidebar_right.search_focused {
39 if ev.state != KeyState::Pressed {
40 return None;
41 }
42 return sidebar_search_key(ev, &model.sidebar_right.search, Msg::SidebarRight);
43 }
44 // El teclado alimenta el campo enfocado (renombrar / semilla / IA).
45 if model.name_focused {
46 return Some(Msg::RenameKey(ev.clone()));
47 }
48 if model.formula_focused {
49 return Some(Msg::FormulaKey(ev.clone()));
50 }
51 if model.cond_formula_focused {
52 return Some(Msg::CondFormulaKey(ev.clone()));
53 }
54 if model.seed_focused {
55 return Some(Msg::SeedKey(ev.clone()));
56 }
57 if model.file_focused {
58 return Some(Msg::FileKey(ev.clone()));
59 }
60 if model.ai_focused {
61 if ev.state == KeyState::Pressed && matches!(&ev.key, Key::Named(NamedKey::Enter)) {
62 return Some(Msg::AiGenerate);
63 }
64 return Some(Msg::AiKey(ev.clone()));
65 }
66 None
67 }
68
69 fn update(model: Model, msg: Msg, handle: &Handle<Msg>) -> Model {
70 actualizar(model, msg, handle)
71 }
72
73 fn view(model: &Model) -> View<Msg> {
74 // Cuerpo: [sidebar niveles · canvas · sidebar herramientas], los DOS
75 // sidebars unificados (widget rag-sidebar). Cada uno reserva su columna
76 // cuando está Fijo (sin autohide); con Flota/Autoesconde flota como overlay
77 // sobre el canvas, con un backdrop de click-away. La orientación de los
78 // dientes la resuelve el widget (izq InnerLeft, der InnerRight) según su eje
79 // Rail Adentro/Afuera (sembrado del viejo `dientes_outside`).
80 let left_fixed = model.sidebar_left.reserves_space();
81 let right_fixed = model.sidebar_right.reserves_space();
82
83 let canvas_grow = View::new(Style {
84 flex_direction: FlexDirection::Row,
85 flex_grow: 1.0,
86 size: Size { width: percent(0.0_f32), height: percent(1.0_f32) },
87 min_size: Size { width: length(0.0_f32), height: length(0.0_f32) },
88 ..Default::default()
89 })
90 .children(vec![center(model)]);
91
92 let mut row_kids: Vec<View<Msg>> = Vec::with_capacity(3);
93 if left_fixed {
94 row_kids.push(left_sidebar(model));
95 }
96 row_kids.push(canvas_grow);
97 if right_fixed {
98 row_kids.push(right_sidebar(model));
99 }
100 let base = View::new(Style {
101 flex_direction: FlexDirection::Row,
102 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
103 ..Default::default()
104 })
105 .fill(model.theme.bg_app)
106 .children(row_kids);
107
108 let mut layers: Vec<View<Msg>> = vec![base];
109 // Sidebars flotantes (Flota/Autoesconde): overlay pegado al borde + backdrop.
110 // Como son selectores fijos, su panel siempre está a la vista mientras no
111 // autoescondan; el backdrop los guarda con Reveal(false) (y el update los
112 // re-abre sobre el nivel/tool activo — nunca colapsan de verdad).
113 if !left_fixed && !model.sidebar_left.hidden() {
114 layers.push(float_backdrop(Msg::SidebarLeft(RagSidebarMsg::Reveal(false))));
115 layers.push(float_overlay(left_sidebar(model), true));
116 }
117 if !right_fixed && !model.sidebar_right.hidden() {
118 layers.push(float_backdrop(Msg::SidebarRight(RagSidebarMsg::Reveal(false))));
119 layers.push(float_overlay(right_sidebar(model), false));
120 }
121 // Multiselect de disposición (los 4 ejes) de cada sidebar como overlay a
122 // nivel raíz cuando su control ⚙ está abierto.
123 if let Some(o) = left_multiselect_overlay(model) {
124 layers.push(o);
125 }
126 if let Some(o) = right_multiselect_overlay(model) {
127 layers.push(o);
128 }
129
130 if layers.len() == 1 {
131 layers.into_iter().next().unwrap_or_else(|| View::new(Style::default()))
132 } else {
133 View::new(Style {
134 position: Position::Relative,
135 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
136 ..Default::default()
137 })
138 .children(layers)
139 }
140 }
141}
142
143// =============================================================================
144// Sidebars
145// =============================================================================
146