use super::*; pub(crate) struct Studio; impl App for Studio { type Model = Model; type Msg = Msg; fn title() -> &'static str { "llimphi-voxel-studio — creador de mundos" } fn initial_size() -> (u32, u32) { (1180, 760) } fn init(handle: &Handle) -> Model { handle.spawn_periodic(Duration::from_millis(33), || Msg::Tick); demo_model() } fn on_wheel(_m: &Model, delta: WheelDelta, _c: (f32, f32), _mods: Modifiers) -> Option { Some(Msg::Zoom(delta.y)) } fn on_key(model: &Model, ev: &KeyEvent) -> Option { // En modo JUEGO el teclado maneja el movimiento, no los campos. if model.jugando { return juego_on_key(ev); } // Buscador de un sidebar unificado con foco: las teclas van al filtro // (Esc/Enter lo sueltan; Backspace borra; el texto tipeado se inserta). if model.sidebar_left.search_focused { if ev.state != KeyState::Pressed { return None; } return sidebar_search_key(ev, &model.sidebar_left.search, Msg::SidebarLeft); } if model.sidebar_right.search_focused { if ev.state != KeyState::Pressed { return None; } return sidebar_search_key(ev, &model.sidebar_right.search, Msg::SidebarRight); } // El teclado alimenta el campo enfocado (renombrar / semilla / IA). if model.name_focused { return Some(Msg::RenameKey(ev.clone())); } if model.formula_focused { return Some(Msg::FormulaKey(ev.clone())); } if model.cond_formula_focused { return Some(Msg::CondFormulaKey(ev.clone())); } if model.seed_focused { return Some(Msg::SeedKey(ev.clone())); } if model.file_focused { return Some(Msg::FileKey(ev.clone())); } if model.ai_focused { if ev.state == KeyState::Pressed && matches!(&ev.key, Key::Named(NamedKey::Enter)) { return Some(Msg::AiGenerate); } return Some(Msg::AiKey(ev.clone())); } None } fn update(model: Model, msg: Msg, handle: &Handle) -> Model { actualizar(model, msg, handle) } fn view(model: &Model) -> View { // Cuerpo: [sidebar niveles · canvas · sidebar herramientas], los DOS // sidebars unificados (widget rag-sidebar). Cada uno reserva su columna // cuando está Fijo (sin autohide); con Flota/Autoesconde flota como overlay // sobre el canvas, con un backdrop de click-away. La orientación de los // dientes la resuelve el widget (izq InnerLeft, der InnerRight) según su eje // Rail Adentro/Afuera (sembrado del viejo `dientes_outside`). let left_fixed = model.sidebar_left.reserves_space(); let right_fixed = model.sidebar_right.reserves_space(); let canvas_grow = View::new(Style { flex_direction: FlexDirection::Row, flex_grow: 1.0, size: Size { width: percent(0.0_f32), height: percent(1.0_f32) }, min_size: Size { width: length(0.0_f32), height: length(0.0_f32) }, ..Default::default() }) .children(vec![center(model)]); let mut row_kids: Vec> = Vec::with_capacity(3); if left_fixed { row_kids.push(left_sidebar(model)); } row_kids.push(canvas_grow); if right_fixed { row_kids.push(right_sidebar(model)); } let base = View::new(Style { flex_direction: FlexDirection::Row, size: Size { width: percent(1.0_f32), height: percent(1.0_f32) }, ..Default::default() }) .fill(model.theme.bg_app) .children(row_kids); let mut layers: Vec> = vec![base]; // Sidebars flotantes (Flota/Autoesconde): overlay pegado al borde + backdrop. // Como son selectores fijos, su panel siempre está a la vista mientras no // autoescondan; el backdrop los guarda con Reveal(false) (y el update los // re-abre sobre el nivel/tool activo — nunca colapsan de verdad). if !left_fixed && !model.sidebar_left.hidden() { layers.push(float_backdrop(Msg::SidebarLeft(RagSidebarMsg::Reveal(false)))); layers.push(float_overlay(left_sidebar(model), true)); } if !right_fixed && !model.sidebar_right.hidden() { layers.push(float_backdrop(Msg::SidebarRight(RagSidebarMsg::Reveal(false)))); layers.push(float_overlay(right_sidebar(model), false)); } // Multiselect de disposición (los 4 ejes) de cada sidebar como overlay a // nivel raíz cuando su control ⚙ está abierto. if let Some(o) = left_multiselect_overlay(model) { layers.push(o); } if let Some(o) = right_multiselect_overlay(model) { layers.push(o); } if layers.len() == 1 { layers.into_iter().next().unwrap_or_else(|| View::new(Style::default())) } else { View::new(Style { position: Position::Relative, size: Size { width: percent(1.0_f32), height: percent(1.0_f32) }, ..Default::default() }) .children(layers) } } } // ============================================================================= // Sidebars // =============================================================================