feat(tahuantinsuyu): UX pass — splitter, light wheel, scroll, zoom/pan, dock lateral
Seis fixes derivados de testing real, ordenados por costo:
- Splitter (yahweh-widget-splitter): `flex-basis: 0` por item para que
el ratio flex-grow se respete sin importar el min-content de los
hijos. Sin esto, al cambiar el canvas de Empty→Wheel (WHEEL_SIZE
fijo de 580px) la suma de basis excedía el contenedor y flexbox
abandonaba el ratio 1:4, aplastando el tree a 0px (síntoma
reportado: "el tree desaparece al seleccionar carta"). También se
amplió la hit-zone del divider de 4px a 12px manteniendo una franja
visual de 4px centrada — la zona de pointer-capture y cursor es
ahora mucho más generosa, el visual sigue fino.
- Light mode wheel (tahuantinsuyu-canvas + tahuantinsuyu-theme): el
gradient del fondo del wheel pasa de alphas 0.06/0.03 (invisibles
contra fondo claro) a 0.18/0.10 cuando el theme es light. Cusps y
aspectos secundarios del light palette bajan luminancia y suben
alpha para no lavarse contra blanco.
- Panel scroll (tahuantinsuyu-panel): body del control panel agrega
`flex_grow + min_h(0) + overflow_y_scroll` para que cuando los
controles no caben aparezca scroll vertical en lugar de cortarse.
- Canvas zoom + pan (tahuantinsuyu-canvas): nuevo estado
view_scale / view_pan_x / view_pan_y. Ctrl+wheel zoomea
multiplicativo (clamp 0.5..3.0); wheel solo paneja. MMB drag para
pan libre. Hotkey `0` resetea zoom+pan. Hit-tests del jog-dial y
hover derivan ahora el `r_outer` del width actual del canvas, así
se autoescalan con el zoom.
- Panel dock lateral (shell.rs): nuevo `PanelDock { Bottom, Right,
Left }` configurable desde 3 botones en el header (◧ ▭ ◨). Bottom
mantiene el layout histórico (tree+canvas / panel); las variantes
laterales colapsan los splitters anidados en uno solo horizontal
de 3 columnas. El dock se persiste en `layout.panel_dock` y cada
layout guarda sus flex en una key distinta para no pisarse.
`load_split_flex_n` / `save_split_flex` generalizados a N hijos.
Tests: 6 pasan (incluye nuevo roundtrip de PanelDock y N-flex).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -24,8 +24,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use gpui::{
|
||||
Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, Window, div,
|
||||
prelude::*, px,
|
||||
ClickEvent, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Window, div, prelude::*, px,
|
||||
};
|
||||
|
||||
use tahuantinsuyu_canvas::{
|
||||
@@ -45,6 +45,35 @@ use yahweh_widget_container_core::ChildSlot;
|
||||
use yahweh_widget_splitter::{SplitContainer, SplitEvent};
|
||||
use yahweh_widget_theme_switcher::theme_switcher;
|
||||
|
||||
/// Posición del panel de control dentro del shell. `Bottom` mantiene
|
||||
/// el layout histórico (tree+canvas arriba, panel abajo); las variantes
|
||||
/// laterales colapsan los splitters anidados en uno solo de 3 columnas.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PanelDock {
|
||||
Bottom,
|
||||
Right,
|
||||
Left,
|
||||
}
|
||||
|
||||
impl PanelDock {
|
||||
fn as_setting(&self) -> &'static str {
|
||||
match self {
|
||||
PanelDock::Bottom => "bottom",
|
||||
PanelDock::Right => "right",
|
||||
PanelDock::Left => "left",
|
||||
}
|
||||
}
|
||||
|
||||
fn from_setting(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"bottom" => Some(PanelDock::Bottom),
|
||||
"right" => Some(PanelDock::Right),
|
||||
"left" => Some(PanelDock::Left),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Status del broker brahman tal como lo vimos en el último ping.
|
||||
/// Se refresca cada 30 segundos desde un background task.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -64,19 +93,24 @@ pub enum BrahmanStatus {
|
||||
|
||||
pub struct Shell {
|
||||
store: Store,
|
||||
/// El árbol vive como child de `outer_split` (vía AnyView clone),
|
||||
/// pero retenemos el Entity acá para que las subscripciones
|
||||
/// registradas en `new` sigan vivas — al droppear el último handle,
|
||||
/// gpui cancela los suscriptores.
|
||||
#[allow(dead_code)]
|
||||
/// Los tres widgets viven como children de los splitters vía
|
||||
/// AnyView clone; retenemos los Entity acá para que las
|
||||
/// subscripciones sigan vivas y para poder rearmar el layout al
|
||||
/// cambiar `dock` sin recrear los widgets.
|
||||
tree: Entity<TahuantinsuyuTree>,
|
||||
canvas: Entity<AstrologyCanvas>,
|
||||
panel: Entity<ControlPanel>,
|
||||
/// Splitter vertical entre el main_row (arriba — tree + canvas) y
|
||||
/// el panel de control (abajo). El splitter horizontal interno se
|
||||
/// arma en `new` y queda referenciado vía `outer_split` (es uno de
|
||||
/// sus children), sin necesidad de retenerlo aparte.
|
||||
/// Splitter "exterior". En dock=Bottom es vertical con (main_split,
|
||||
/// panel) como hijos; en dock=Right/Left es horizontal y agrupa
|
||||
/// tree+canvas+panel en una sola tira.
|
||||
outer_split: Entity<SplitContainer>,
|
||||
/// Splitter horizontal interno con (tree, canvas). Solo se usa
|
||||
/// cuando dock=Bottom; en docks laterales queda vivo pero sin ser
|
||||
/// hijo del árbol activo.
|
||||
main_split: Entity<SplitContainer>,
|
||||
/// Dock activo del panel — determina cómo se arman los splitters
|
||||
/// y cuáles flex se persisten.
|
||||
dock: PanelDock,
|
||||
/// Último estado conocido del broker brahman — refrescado cada
|
||||
/// 30s desde el background task.
|
||||
brahman_status: BrahmanStatus,
|
||||
@@ -125,90 +159,193 @@ impl Shell {
|
||||
})
|
||||
.detach();
|
||||
|
||||
// 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);
|
||||
// Splitters vacíos — `apply_dock` los puebla según el layout
|
||||
// activo. Horizontal/Vertical son defaults; cada apply ajusta la
|
||||
// dirección antes de setear children.
|
||||
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: main_left,
|
||||
label: None,
|
||||
view: gpui::AnyView::from(tree.clone()),
|
||||
},
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-canvas"),
|
||||
flex: main_right,
|
||||
label: None,
|
||||
view: gpui::AnyView::from(canvas.clone()),
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
let outer_split = cx.new(|cx| SplitContainer::new(LayoutDirection::Vertical, cx));
|
||||
|
||||
// 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: outer_top,
|
||||
label: None,
|
||||
view: gpui::AnyView::from(main_split.clone()),
|
||||
},
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-panel"),
|
||||
flex: outer_bottom,
|
||||
label: None,
|
||||
view: gpui::AnyView::from(panel.clone()),
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
sc
|
||||
});
|
||||
|
||||
// Persistir flex en `DragEnd`. Capturamos el store por valor
|
||||
// (Store es Clone — comparte el Arc<Mutex<Connection>>).
|
||||
// Persistir flex en `DragEnd`. La key del setting depende del
|
||||
// dock activo, así no se pisan los flexes de un layout con los
|
||||
// de otro al mudarse. Se lee dentro del closure para tomar el
|
||||
// dock actualizado, no el capturado en `new`.
|
||||
let store_main = store.clone();
|
||||
cx.subscribe(&main_split, move |_, sc, ev: &SplitEvent, cx| {
|
||||
cx.subscribe(&main_split, move |this: &mut Self, sc, ev: &SplitEvent, cx| {
|
||||
if matches!(ev, SplitEvent::DragEnd) {
|
||||
save_split_flex(&store_main, "layout.main_split", sc.read(cx));
|
||||
let key = split_key_main(this.dock);
|
||||
save_split_flex(&store_main, key, sc.read(cx));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
let store_outer = store.clone();
|
||||
cx.subscribe(&outer_split, move |_, sc, ev: &SplitEvent, cx| {
|
||||
cx.subscribe(&outer_split, move |this: &mut Self, sc, ev: &SplitEvent, cx| {
|
||||
if matches!(ev, SplitEvent::DragEnd) {
|
||||
save_split_flex(&store_outer, "layout.outer_split", sc.read(cx));
|
||||
let key = split_key_outer(this.dock);
|
||||
save_split_flex(&store_outer, key, sc.read(cx));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let shell = Self {
|
||||
let dock = load_dock(&store).unwrap_or(PanelDock::Bottom);
|
||||
|
||||
let mut shell = Self {
|
||||
store,
|
||||
tree,
|
||||
canvas,
|
||||
panel,
|
||||
outer_split,
|
||||
main_split,
|
||||
dock,
|
||||
brahman_status: BrahmanStatus::Pending,
|
||||
current_chart: None,
|
||||
current_offset_minutes: 0,
|
||||
module_configs: HashMap::new(),
|
||||
render_seq: 0,
|
||||
};
|
||||
shell.apply_dock(dock, cx);
|
||||
shell.refresh_chart_options(cx);
|
||||
shell.spawn_brahman_status_loop(cx);
|
||||
shell
|
||||
}
|
||||
|
||||
/// Arma el árbol de splitters según el dock pedido y persiste la
|
||||
/// elección. Idempotente: llamar con el dock actual reconstruye los
|
||||
/// children con flexes leídos del setting (útil tras `new`).
|
||||
pub fn apply_dock(&mut self, dock: PanelDock, cx: &mut Context<Self>) {
|
||||
self.dock = dock;
|
||||
|
||||
let tree_view = gpui::AnyView::from(self.tree.clone());
|
||||
let canvas_view = gpui::AnyView::from(self.canvas.clone());
|
||||
let panel_view = gpui::AnyView::from(self.panel.clone());
|
||||
let main_view = gpui::AnyView::from(self.main_split.clone());
|
||||
|
||||
match dock {
|
||||
PanelDock::Bottom => {
|
||||
let flex_main = load_split_flex_n(
|
||||
&self.store,
|
||||
split_key_main(dock),
|
||||
&[1.0, 4.0],
|
||||
);
|
||||
let flex_outer = load_split_flex_n(
|
||||
&self.store,
|
||||
split_key_outer(dock),
|
||||
&[4.0, 1.0],
|
||||
);
|
||||
self.main_split.update(cx, |sc, cx| {
|
||||
sc.set_direction(LayoutDirection::Horizontal, cx);
|
||||
sc.set_children(
|
||||
vec![
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-tree"),
|
||||
flex: flex_main[0],
|
||||
label: None,
|
||||
view: tree_view.clone(),
|
||||
},
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-canvas"),
|
||||
flex: flex_main[1],
|
||||
label: None,
|
||||
view: canvas_view.clone(),
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
self.outer_split.update(cx, |sc, cx| {
|
||||
sc.set_direction(LayoutDirection::Vertical, cx);
|
||||
sc.set_children(
|
||||
vec![
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-main"),
|
||||
flex: flex_outer[0],
|
||||
label: None,
|
||||
view: main_view,
|
||||
},
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-panel"),
|
||||
flex: flex_outer[1],
|
||||
label: None,
|
||||
view: panel_view,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
PanelDock::Right => {
|
||||
let flex = load_split_flex_n(
|
||||
&self.store,
|
||||
split_key_outer(dock),
|
||||
&[1.0, 4.0, 1.5],
|
||||
);
|
||||
self.outer_split.update(cx, |sc, cx| {
|
||||
sc.set_direction(LayoutDirection::Horizontal, cx);
|
||||
sc.set_children(
|
||||
vec![
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-tree"),
|
||||
flex: flex[0],
|
||||
label: None,
|
||||
view: tree_view,
|
||||
},
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-canvas"),
|
||||
flex: flex[1],
|
||||
label: None,
|
||||
view: canvas_view,
|
||||
},
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-panel"),
|
||||
flex: flex[2],
|
||||
label: None,
|
||||
view: panel_view,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
PanelDock::Left => {
|
||||
let flex = load_split_flex_n(
|
||||
&self.store,
|
||||
split_key_outer(dock),
|
||||
&[1.5, 1.0, 4.0],
|
||||
);
|
||||
self.outer_split.update(cx, |sc, cx| {
|
||||
sc.set_direction(LayoutDirection::Horizontal, cx);
|
||||
sc.set_children(
|
||||
vec![
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-panel"),
|
||||
flex: flex[0],
|
||||
label: None,
|
||||
view: panel_view,
|
||||
},
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-tree"),
|
||||
flex: flex[1],
|
||||
label: None,
|
||||
view: tree_view,
|
||||
},
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-canvas"),
|
||||
flex: flex[2],
|
||||
label: None,
|
||||
view: canvas_view,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = self.store.set_setting("layout.panel_dock", dock.as_setting()) {
|
||||
eprintln!("[shell] persist panel_dock: {}", e);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Loop que cada 30s pregunta al broker la lista de sessions
|
||||
/// activas y actualiza `brahman_status`. El cómputo bloqueante
|
||||
/// (list_sessions_blocking abre su propio tokio runtime) corre en
|
||||
@@ -918,35 +1055,122 @@ 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) {
|
||||
/// Lee del `settings` el flex de un splitter en formato "f0,f1,..." y
|
||||
/// lo devuelve como `Vec<f32>` con la misma longitud que `defaults`.
|
||||
/// Si no hay nada persistido, faltan campos, o algún flex es ≤0, cae a
|
||||
/// `defaults`. Validación estricta porque un flex 0 colapsa al panel.
|
||||
fn load_split_flex_n(store: &Store, key: &str, defaults: &[f32]) -> Vec<f32> {
|
||||
let Ok(Some(raw)) = store.get_setting(key) else {
|
||||
return (default_a, default_b);
|
||||
return defaults.to_vec();
|
||||
};
|
||||
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),
|
||||
let parsed: Vec<f32> = raw
|
||||
.split(',')
|
||||
.filter_map(|s| s.trim().parse::<f32>().ok())
|
||||
.collect();
|
||||
if parsed.len() != defaults.len() || parsed.iter().any(|&f| f <= 0.0) {
|
||||
return defaults.to_vec();
|
||||
}
|
||||
parsed
|
||||
}
|
||||
|
||||
/// Persiste los flex actuales de un splitter — soporta N children.
|
||||
fn save_split_flex(store: &Store, key: &str, sc: &SplitContainer) {
|
||||
let children = sc.children();
|
||||
if children.is_empty() {
|
||||
return;
|
||||
}
|
||||
let payload: String = children
|
||||
.iter()
|
||||
.map(|c| format!("{:.4}", c.flex))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
if let Err(e) = store.set_setting(key, &payload) {
|
||||
eprintln!("[shell] save_split_flex {}: {}", key, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
/// Key del setting donde se persiste el splitter "outer" (el de mayor
|
||||
/// nivel del árbol). En dock=Bottom guarda (main,panel); en docks
|
||||
/// laterales guarda los flex de las 3 columnas — usamos keys distintas
|
||||
/// para no pisar valores entre layouts.
|
||||
fn split_key_outer(dock: PanelDock) -> &'static str {
|
||||
match dock {
|
||||
PanelDock::Bottom => "layout.outer_split",
|
||||
PanelDock::Right => "layout.dock_right",
|
||||
PanelDock::Left => "layout.dock_left",
|
||||
}
|
||||
}
|
||||
|
||||
/// Key del setting del splitter horizontal interno. Solo se usa cuando
|
||||
/// dock=Bottom (en docks laterales no hay main_split activo).
|
||||
fn split_key_main(dock: PanelDock) -> &'static str {
|
||||
match dock {
|
||||
PanelDock::Bottom => "layout.main_split",
|
||||
// En docks laterales el main_split está dormido — escribir acá
|
||||
// no hace daño pero tampoco se usa al recargar.
|
||||
PanelDock::Right => "layout.main_split_right",
|
||||
PanelDock::Left => "layout.main_split_left",
|
||||
}
|
||||
}
|
||||
|
||||
fn load_dock(store: &Store) -> Option<PanelDock> {
|
||||
let raw = store.get_setting("layout.panel_dock").ok().flatten()?;
|
||||
PanelDock::from_setting(raw.trim())
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
/// Tres botones compactos en el header — uno por dock disponible.
|
||||
/// El dock activo se marca con `bg=accent`; los demás van planos.
|
||||
/// Click llama a `apply_dock` que reorganiza splitters y persiste.
|
||||
fn render_dock_switcher(
|
||||
&self,
|
||||
theme: &yahweh_theme::Theme,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let mut row = div()
|
||||
.id("tts-dock-switcher")
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap(px(2.0))
|
||||
.px(px(2.0))
|
||||
.py(px(2.0))
|
||||
.rounded(px(6.0))
|
||||
.bg(theme.bg_panel_alt.clone())
|
||||
.border_1()
|
||||
.border_color(theme.border);
|
||||
|
||||
for (dock, glyph) in [
|
||||
(PanelDock::Left, "◧"),
|
||||
(PanelDock::Bottom, "▭"),
|
||||
(PanelDock::Right, "◨"),
|
||||
] {
|
||||
let active = self.dock == dock;
|
||||
let fg = if active { theme.fg_text } else { theme.fg_muted };
|
||||
let id: SharedString = SharedString::from(format!("tts-dock-{}", dock.as_setting()));
|
||||
let mut btn = div()
|
||||
.id(gpui::ElementId::from(id))
|
||||
.w(px(22.0))
|
||||
.h(px(20.0))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded(px(4.0))
|
||||
.text_size(px(12.0))
|
||||
.text_color(fg)
|
||||
.hover(|s| s.bg(theme.bg_row_hover))
|
||||
.child(SharedString::from(glyph))
|
||||
.on_click(cx.listener(move |this, _: &ClickEvent, _w, cx| {
|
||||
if this.dock != dock {
|
||||
this.apply_dock(dock, cx);
|
||||
}
|
||||
}));
|
||||
if active {
|
||||
btn = btn.bg(theme.accent);
|
||||
}
|
||||
row = row.child(btn);
|
||||
}
|
||||
|
||||
row
|
||||
}
|
||||
}
|
||||
|
||||
@@ -999,6 +1223,7 @@ impl Render for Shell {
|
||||
.child("estudio de astrología profesional"),
|
||||
)
|
||||
.child(div().flex_grow())
|
||||
.child(self.render_dock_switcher(&theme, cx))
|
||||
.child(brahman_badge)
|
||||
.child(theme_switcher(cx));
|
||||
|
||||
@@ -1171,23 +1396,55 @@ mod tests {
|
||||
|
||||
/// El flex de los splitters persiste entre instancias de Shell que
|
||||
/// comparten la misma store (in-memory): primera shell escribe via
|
||||
/// `save_split_flex`, segunda shell lee via `load_split_flex` al
|
||||
/// boot.
|
||||
/// `save_split_flex`, segunda shell lee via `load_split_flex_n` al
|
||||
/// boot. Cubre 2 y 3 hijos (Bottom vs docks laterales).
|
||||
#[test]
|
||||
fn split_flex_round_trip_via_store() {
|
||||
let store = Store::in_memory().expect("store");
|
||||
// No hay nada persistido todavía: defaults.
|
||||
assert_eq!(load_split_flex(&store, "layout.x", 1.0, 4.0), (1.0, 4.0));
|
||||
let defaults_2 = vec![1.0_f32, 4.0];
|
||||
let defaults_3 = vec![1.0_f32, 4.0, 1.5];
|
||||
|
||||
// Sin nada persistido → defaults.
|
||||
assert_eq!(load_split_flex_n(&store, "layout.x", &defaults_2), defaults_2);
|
||||
|
||||
store.set_setting("layout.x", "2.5,3.5").unwrap();
|
||||
assert_eq!(load_split_flex(&store, "layout.x", 1.0, 4.0), (2.5, 3.5));
|
||||
assert_eq!(
|
||||
load_split_flex_n(&store, "layout.x", &defaults_2),
|
||||
vec![2.5_f32, 3.5]
|
||||
);
|
||||
|
||||
store.set_setting("layout.x", "1.0,4.0,2.0").unwrap();
|
||||
assert_eq!(
|
||||
load_split_flex_n(&store, "layout.x", &defaults_3),
|
||||
vec![1.0_f32, 4.0, 2.0]
|
||||
);
|
||||
|
||||
// Valor corrupto → defaults.
|
||||
store.set_setting("layout.x", "garbage").unwrap();
|
||||
assert_eq!(load_split_flex(&store, "layout.x", 1.0, 4.0), (1.0, 4.0));
|
||||
assert_eq!(load_split_flex_n(&store, "layout.x", &defaults_2), defaults_2);
|
||||
|
||||
// Valores ≤ 0 → defaults (los splitters tratan 0 como hidden).
|
||||
// Cantidad incorrecta → defaults.
|
||||
store.set_setting("layout.x", "2,3,4").unwrap();
|
||||
assert_eq!(load_split_flex_n(&store, "layout.x", &defaults_2), defaults_2);
|
||||
|
||||
// Valor ≤0 → defaults.
|
||||
store.set_setting("layout.x", "0,5").unwrap();
|
||||
assert_eq!(load_split_flex(&store, "layout.x", 1.0, 4.0), (1.0, 4.0));
|
||||
assert_eq!(load_split_flex_n(&store, "layout.x", &defaults_2), defaults_2);
|
||||
}
|
||||
|
||||
/// PanelDock roundtrip via store.
|
||||
#[test]
|
||||
fn panel_dock_setting_roundtrip() {
|
||||
assert_eq!(PanelDock::from_setting("bottom"), Some(PanelDock::Bottom));
|
||||
assert_eq!(PanelDock::from_setting("right"), Some(PanelDock::Right));
|
||||
assert_eq!(PanelDock::from_setting("left"), Some(PanelDock::Left));
|
||||
assert_eq!(PanelDock::from_setting("nope"), None);
|
||||
|
||||
let store = Store::in_memory().expect("store");
|
||||
assert_eq!(load_dock(&store), None);
|
||||
store
|
||||
.set_setting("layout.panel_dock", PanelDock::Right.as_setting())
|
||||
.unwrap();
|
||||
assert_eq!(load_dock(&store), Some(PanelDock::Right));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user