feat(tahuantinsuyu): persistir flex de los splitters entre sesiones
Hasta ahora cada boot reseteaba los splitters al default (1:4 horizontal, 4:1 vertical), forzando a rearrastrar manualmente cada vez. Ahora el flex se guarda en la tabla `settings` ya existente. - `tahuantinsuyu-store`: nuevos `get_setting`/`set_setting` con upsert + test de roundtrip. - `tahuantinsuyu` shell: al boot, `load_split_flex` lee `layout.main_split` y `layout.outer_split` (formato "a,b" como texto). Si no hay entry o está corrupto cae a defaults. - Subscribe a `SplitEvent::DragEnd` en cada splitter — `save_split_flex` escribe los flex actuales al settings. Mouseup-driven, no cada-frame: 0 escrituras durante el drag, 1 al final. `module_configs` ya estaba persistido por carta vía la tabla `module_state` (`persist_module` + `load_persisted_module_states`), no requiere cambios. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -42,7 +42,7 @@ use tahuantinsuyu_tree::{parse_city_atlas_tsv, TahuantinsuyuTree, TreeEvent};
|
|||||||
use yahweh_core::{LayoutDirection, NodeId};
|
use yahweh_core::{LayoutDirection, NodeId};
|
||||||
use yahweh_theme::Theme;
|
use yahweh_theme::Theme;
|
||||||
use yahweh_widget_container_core::ChildSlot;
|
use yahweh_widget_container_core::ChildSlot;
|
||||||
use yahweh_widget_splitter::SplitContainer;
|
use yahweh_widget_splitter::{SplitContainer, SplitEvent};
|
||||||
use yahweh_widget_theme_switcher::theme_switcher;
|
use yahweh_widget_theme_switcher::theme_switcher;
|
||||||
|
|
||||||
/// Status del broker brahman tal como lo vimos en el último ping.
|
/// Status del broker brahman tal como lo vimos en el último ping.
|
||||||
@@ -125,20 +125,23 @@ impl Shell {
|
|||||||
})
|
})
|
||||||
.detach();
|
.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));
|
let main_split = cx.new(|cx| SplitContainer::new(LayoutDirection::Horizontal, cx));
|
||||||
main_split.update(cx, |sc, cx| {
|
main_split.update(cx, |sc, cx| {
|
||||||
sc.set_children(
|
sc.set_children(
|
||||||
vec![
|
vec![
|
||||||
ChildSlot {
|
ChildSlot {
|
||||||
id: NodeId::new("tts-tree"),
|
id: NodeId::new("tts-tree"),
|
||||||
flex: 1.0,
|
flex: main_left,
|
||||||
label: None,
|
label: None,
|
||||||
view: gpui::AnyView::from(tree.clone()),
|
view: gpui::AnyView::from(tree.clone()),
|
||||||
},
|
},
|
||||||
ChildSlot {
|
ChildSlot {
|
||||||
id: NodeId::new("tts-canvas"),
|
id: NodeId::new("tts-canvas"),
|
||||||
flex: 4.0,
|
flex: main_right,
|
||||||
label: None,
|
label: None,
|
||||||
view: gpui::AnyView::from(canvas.clone()),
|
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 outer_split = cx.new(|cx| {
|
||||||
let mut sc = SplitContainer::new(LayoutDirection::Vertical, cx);
|
let mut sc = SplitContainer::new(LayoutDirection::Vertical, cx);
|
||||||
sc.set_children(
|
sc.set_children(
|
||||||
vec![
|
vec![
|
||||||
ChildSlot {
|
ChildSlot {
|
||||||
id: NodeId::new("tts-main"),
|
id: NodeId::new("tts-main"),
|
||||||
flex: 4.0,
|
flex: outer_top,
|
||||||
label: None,
|
label: None,
|
||||||
view: gpui::AnyView::from(main_split.clone()),
|
view: gpui::AnyView::from(main_split.clone()),
|
||||||
},
|
},
|
||||||
ChildSlot {
|
ChildSlot {
|
||||||
id: NodeId::new("tts-panel"),
|
id: NodeId::new("tts-panel"),
|
||||||
flex: 1.0,
|
flex: outer_bottom,
|
||||||
label: None,
|
label: None,
|
||||||
view: gpui::AnyView::from(panel.clone()),
|
view: gpui::AnyView::from(panel.clone()),
|
||||||
},
|
},
|
||||||
@@ -170,6 +175,23 @@ impl Shell {
|
|||||||
sc
|
sc
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Persistir flex en `DragEnd`. Capturamos el store por valor
|
||||||
|
// (Store es Clone — comparte el Arc<Mutex<Connection>>).
|
||||||
|
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 {
|
let shell = Self {
|
||||||
store,
|
store,
|
||||||
tree,
|
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::<f32>().ok());
|
||||||
|
let b = parts.next().and_then(|s| s.trim().parse::<f32>().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 {
|
impl Render for Shell {
|
||||||
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let theme = Theme::global(cx).clone();
|
let theme = Theme::global(cx).clone();
|
||||||
|
|||||||
@@ -419,6 +419,35 @@ impl Store {
|
|||||||
Ok(out)
|
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<Option<String>> {
|
||||||
|
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)
|
// Recursive descent: charts under a group/contact (para thumbnails)
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
@@ -676,6 +705,23 @@ mod tests {
|
|||||||
assert_eq!(by_id["transit"].enabled, false);
|
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]
|
#[test]
|
||||||
fn full_hierarchy_roundtrip() {
|
fn full_hierarchy_roundtrip() {
|
||||||
let s = Store::in_memory().unwrap();
|
let s = Store::in_memory().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user