tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ main llimphi-voxel-studio/src/app.rs Rust · 146 líneas · 5.5 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · Sergio ·

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