diff --git a/crates/apps/tahuantinsuyu/src/shell.rs b/crates/apps/tahuantinsuyu/src/shell.rs index 4623f5c..f135af4 100644 --- a/crates/apps/tahuantinsuyu/src/shell.rs +++ b/crates/apps/tahuantinsuyu/src/shell.rs @@ -42,7 +42,7 @@ use tahuantinsuyu_tree::{parse_city_atlas_tsv, TahuantinsuyuTree, TreeEvent}; use yahweh_core::{LayoutDirection, NodeId}; use yahweh_theme::Theme; use yahweh_widget_container_core::ChildSlot; -use yahweh_widget_splitter::SplitContainer; +use yahweh_widget_splitter::{SplitContainer, SplitEvent}; use yahweh_widget_theme_switcher::theme_switcher; /// Status del broker brahman tal como lo vimos en el último ping. @@ -125,20 +125,23 @@ impl Shell { }) .detach(); - // Splitter horizontal: tree + canvas (flex 1 : 4). + // Splitter horizontal: tree + canvas. Defaults (1.0, 4.0) salvo + // que tengamos un flex persistido en `settings`. + let (main_left, main_right) = + load_split_flex(&store, "layout.main_split", 1.0, 4.0); let main_split = cx.new(|cx| SplitContainer::new(LayoutDirection::Horizontal, cx)); main_split.update(cx, |sc, cx| { sc.set_children( vec![ ChildSlot { id: NodeId::new("tts-tree"), - flex: 1.0, + flex: main_left, label: None, view: gpui::AnyView::from(tree.clone()), }, ChildSlot { id: NodeId::new("tts-canvas"), - flex: 4.0, + flex: main_right, label: None, view: gpui::AnyView::from(canvas.clone()), }, @@ -147,20 +150,22 @@ impl Shell { ); }); - // Splitter vertical: main_split arriba, panel abajo (flex 4 : 1). + // Splitter vertical: main arriba, panel abajo. Defaults (4.0, 1.0). + let (outer_top, outer_bottom) = + load_split_flex(&store, "layout.outer_split", 4.0, 1.0); let outer_split = cx.new(|cx| { let mut sc = SplitContainer::new(LayoutDirection::Vertical, cx); sc.set_children( vec![ ChildSlot { id: NodeId::new("tts-main"), - flex: 4.0, + flex: outer_top, label: None, view: gpui::AnyView::from(main_split.clone()), }, ChildSlot { id: NodeId::new("tts-panel"), - flex: 1.0, + flex: outer_bottom, label: None, view: gpui::AnyView::from(panel.clone()), }, @@ -170,6 +175,23 @@ impl Shell { sc }); + // Persistir flex en `DragEnd`. Capturamos el store por valor + // (Store es Clone — comparte el Arc>). + let store_main = store.clone(); + cx.subscribe(&main_split, move |_, sc, ev: &SplitEvent, cx| { + if matches!(ev, SplitEvent::DragEnd) { + save_split_flex(&store_main, "layout.main_split", sc.read(cx)); + } + }) + .detach(); + let store_outer = store.clone(); + cx.subscribe(&outer_split, move |_, sc, ev: &SplitEvent, cx| { + if matches!(ev, SplitEvent::DragEnd) { + save_split_flex(&store_outer, "layout.outer_split", sc.read(cx)); + } + }) + .detach(); + let shell = Self { store, tree, @@ -896,6 +918,38 @@ fn set_module_enabled( } } +/// Lee del `settings` el flex de un splitter (formato "left,right"). Si +/// no hay nada persistido o está corrupto, devuelve los defaults. +fn load_split_flex(store: &Store, key: &str, default_a: f32, default_b: f32) -> (f32, f32) { + let Ok(Some(raw)) = store.get_setting(key) else { + return (default_a, default_b); + }; + let mut parts = raw.split(','); + let a = parts.next().and_then(|s| s.trim().parse::().ok()); + let b = parts.next().and_then(|s| s.trim().parse::().ok()); + match (a, b) { + (Some(a), Some(b)) if a > 0.0 && b > 0.0 => (a, b), + _ => (default_a, default_b), + } +} + +/// Persiste los flex actuales de un splitter de 2 children. Si tiene +/// más children (en el futuro) sólo guarda los dos primeros — ajustar +/// el formato si se necesita más. +fn save_split_flex(store: &Store, key: &str, sc: &SplitContainer) { + let children = sc.children(); + let Some((first, rest)) = children.split_first() else { + return; + }; + let Some(second) = rest.first() else { + return; + }; + let payload = format!("{:.4},{:.4}", first.flex, second.flex); + if let Err(e) = store.set_setting(key, &payload) { + eprintln!("[shell] save_split_flex {}: {}", key, e); + } +} + impl Render for Shell { fn render(&mut self, _w: &mut Window, cx: &mut Context) -> impl IntoElement { let theme = Theme::global(cx).clone(); diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-store/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-store/src/lib.rs index 6742cd9..e93835a 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-store/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-store/src/lib.rs @@ -419,6 +419,35 @@ impl Store { Ok(out) } + // ----------------------------------------------------------------- + // Settings (key/value libre — layout, last-opened chart, etc.) + // ----------------------------------------------------------------- + + /// Lee un valor de la tabla `settings`. `None` si no existe. + pub fn get_setting(&self, key: &str) -> StoreResult> { + let conn = self.conn.lock().unwrap(); + let val = conn + .query_row( + "SELECT value FROM settings WHERE key = ?1", + params![key], + |row| row.get::<_, String>(0), + ) + .optional()?; + Ok(val) + } + + /// Upsert un setting. El valor es texto libre — para JSON, el caller + /// serializa antes de llamar. + pub fn set_setting(&self, key: &str, value: &str) -> StoreResult<()> { + let conn = self.conn.lock().unwrap(); + conn.execute( + "INSERT INTO settings (key, value) VALUES (?1, ?2) \ + ON CONFLICT(key) DO UPDATE SET value = excluded.value", + params![key, value], + )?; + Ok(()) + } + // ----------------------------------------------------------------- // Recursive descent: charts under a group/contact (para thumbnails) // ----------------------------------------------------------------- @@ -676,6 +705,23 @@ mod tests { assert_eq!(by_id["transit"].enabled, false); } + #[test] + fn settings_upsert_and_read() { + let s = Store::in_memory().unwrap(); + assert_eq!(s.get_setting("layout.outer").unwrap(), None); + s.set_setting("layout.outer", "4.0,1.0").unwrap(); + assert_eq!( + s.get_setting("layout.outer").unwrap().as_deref(), + Some("4.0,1.0") + ); + // Upsert — el segundo set sobreescribe. + s.set_setting("layout.outer", "3.5,1.5").unwrap(); + assert_eq!( + s.get_setting("layout.outer").unwrap().as_deref(), + Some("3.5,1.5") + ); + } + #[test] fn full_hierarchy_roundtrip() { let s = Store::in_memory().unwrap();