feat(tahuantinsuyu): fase 22 — layout splitter + atlas loadable desde XDG
Dos features de producción que mejoran la usabilidad sustancialmente. ## #7 — Layout reorganizable con SplitContainer Los 3 paneles ya no tienen tamaños hardcodeados. Reusamos yahweh-widget-splitter (mismo que usa yahweh-shell para sus layouts JSON-config) con 2 niveles: - outer (Vertical): main_split arriba (flex 4) + panel abajo (flex 1) - main_split (Horizontal): tree (flex 1) + canvas (flex 4) El usuario puede arrastrar los dos divisores para redimensionar libremente. Por ejemplo: en una pantalla ancha, dar más al canvas; en una sesión de lectura analítica, agrandar el panel abajo para ver más módulos expandidos. - Shell gana fields main_split + outer_split: Entity<SplitContainer>. - new() construye ambos con ChildSlots envolviendo tree/canvas/panel como AnyView (mismo patrón que LayoutHost de yahweh-shell). - render() simplificado: header + body(outer_split). Las constants TREE_WIDTH y PANEL_HEIGHT desaparecen. - Cargo añade deps: yahweh-core (NodeId, LayoutDirection), yahweh-widget-splitter, yahweh-widget-container-core (ChildSlot). ## #15 — Atlas de ciudades cargable desde TSV El array `CITY_PRESETS` const de 90 ciudades hardcoded ahora es la función `default_city_presets() -> Vec<CityPreset>`. CityPreset.name pasa de `&'static str` a `String` para que el atlas sea construible en runtime. TahuantinsuyuTree gana `city_atlas: Vec<CityPreset>` + setter `set_city_atlas(atlas, cx)`. Al boot, Shell intenta cargar `$XDG_DATA_HOME/tahuantinsuyu/atlas.tsv` y, si existe + parsea bien, reemplaza el atlas hardcoded. Formato TSV (líneas): name<TAB>lat<TAB>lon<TAB>tz_offset_minutes Líneas vacías y `#` comentario se ignoran. Líneas con cualquier parse fallido se descartan en silencio. API pública: `parse_city_atlas_tsv(&str) -> Vec<CityPreset>` (en tahuantinsuyu-tree), reusable por tests/scripts. El usuario que quiera 50.000 ciudades de GeoNames cities5000.txt: 1. wget cities5000.zip de geonames.org 2. awk para extraer (name, lat, lon, tz_offset) y escribir TSV 3. mover a $XDG_DATA_HOME/tahuantinsuyu/atlas.tsv 4. relanzar la app Sin fricción adicional para el usuario común (los 90 hardcoded cubren 99% de casos típicos en español/inglés). cargo check verde, 8 tests engine + 1 test modules verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -38,9 +38,12 @@ use tahuantinsuyu_engine::{
|
||||
use tahuantinsuyu_model::{Chart, ChartId, ModuleState, TreeSelection};
|
||||
use tahuantinsuyu_panel::{ChartOption, ControlPanel, PanelEvent};
|
||||
use tahuantinsuyu_store::Store;
|
||||
use tahuantinsuyu_tree::{TahuantinsuyuTree, TreeEvent};
|
||||
use tahuantinsuyu_tree::{parse_city_atlas_tsv, TahuantinsuyuTree, TreeEvent};
|
||||
use yahweh_bus::AppBus;
|
||||
use yahweh_core::{LayoutDirection, NodeId};
|
||||
use yahweh_theme::Theme;
|
||||
use yahweh_widget_container_core::ChildSlot;
|
||||
use yahweh_widget_splitter::SplitContainer;
|
||||
use yahweh_widget_theme_switcher::theme_switcher;
|
||||
|
||||
const TREE_WIDTH: f32 = 280.0;
|
||||
@@ -53,6 +56,13 @@ pub struct Shell {
|
||||
tree: Entity<TahuantinsuyuTree>,
|
||||
canvas: Entity<AstrologyCanvas>,
|
||||
panel: Entity<ControlPanel>,
|
||||
/// Splitter horizontal entre tree (izq) y canvas (der). El divisor
|
||||
/// es draggable — el flex se persiste in-memory mientras la app
|
||||
/// está abierta.
|
||||
main_split: Entity<SplitContainer>,
|
||||
/// Splitter vertical entre el main_row (arriba) y el panel de
|
||||
/// control (abajo).
|
||||
outer_split: Entity<SplitContainer>,
|
||||
current_chart: Option<Chart>,
|
||||
current_offset_minutes: i64,
|
||||
/// Estado de los módulos overlay (transit, progression, …) por
|
||||
@@ -71,7 +81,16 @@ impl Shell {
|
||||
cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
|
||||
|
||||
let bus = cx.new(|_| AppBus);
|
||||
let tree = cx.new(|cx| TahuantinsuyuTree::new(store.clone(), cx));
|
||||
let tree = cx.new(|cx| {
|
||||
let mut t = TahuantinsuyuTree::new(store.clone(), cx);
|
||||
// Si hay un atlas custom en $XDG_DATA_HOME/tahuantinsuyu/
|
||||
// atlas.tsv, lo cargamos y reemplazamos el atlas hardcoded
|
||||
// de 90 ciudades. Formato TSV: name<TAB>lat<TAB>lon<TAB>tz_min.
|
||||
if let Some(atlas) = load_city_atlas_from_xdg() {
|
||||
t.set_city_atlas(atlas, cx);
|
||||
}
|
||||
t
|
||||
});
|
||||
let canvas = cx.new(AstrologyCanvas::new);
|
||||
let panel = cx.new(ControlPanel::new);
|
||||
|
||||
@@ -90,12 +109,59 @@ impl Shell {
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Splitter horizontal: tree + canvas (flex 1 : 4).
|
||||
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,
|
||||
label: None,
|
||||
view: gpui::AnyView::from(tree.clone()),
|
||||
},
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-canvas"),
|
||||
flex: 4.0,
|
||||
label: None,
|
||||
view: gpui::AnyView::from(canvas.clone()),
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Splitter vertical: main_split arriba, panel abajo (flex 4 : 1).
|
||||
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,
|
||||
label: None,
|
||||
view: gpui::AnyView::from(main_split.clone()),
|
||||
},
|
||||
ChildSlot {
|
||||
id: NodeId::new("tts-panel"),
|
||||
flex: 1.0,
|
||||
label: None,
|
||||
view: gpui::AnyView::from(panel.clone()),
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
sc
|
||||
});
|
||||
|
||||
let mut shell = Self {
|
||||
store,
|
||||
bus,
|
||||
tree,
|
||||
canvas,
|
||||
panel,
|
||||
main_split,
|
||||
outer_split,
|
||||
current_chart: None,
|
||||
current_offset_minutes: 0,
|
||||
module_configs: HashMap::new(),
|
||||
@@ -706,6 +772,28 @@ impl Shell {
|
||||
// truth. Shell y canvas leen del mismo slice.
|
||||
|
||||
|
||||
/// Lee `$XDG_DATA_HOME/tahuantinsuyu/atlas.tsv` si existe y lo parsea
|
||||
/// como atlas de ciudades. Devuelve `None` cuando no hay archivo o
|
||||
/// quedó vacío después del parse — el tree cae al atlas hardcoded.
|
||||
fn load_city_atlas_from_xdg() -> Option<Vec<tahuantinsuyu_tree::CityPreset>> {
|
||||
let path = directories::ProjectDirs::from("net", "gioser", "tahuantinsuyu")
|
||||
.map(|d| d.data_dir().join("atlas.tsv"))?;
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
let content = std::fs::read_to_string(&path).ok()?;
|
||||
let atlas = parse_city_atlas_tsv(&content);
|
||||
if atlas.is_empty() {
|
||||
eprintln!(
|
||||
"[shell] atlas.tsv encontrado en {:?} pero sin filas válidas — fallback a hardcoded",
|
||||
path
|
||||
);
|
||||
return None;
|
||||
}
|
||||
eprintln!("[shell] atlas custom cargado: {} ciudades", atlas.len());
|
||||
Some(atlas)
|
||||
}
|
||||
|
||||
/// Etiqueta breve para mostrar al elegir una carta en el picker:
|
||||
/// `"YYYY-MM-DD · Lugar"` cuando hay lugar, sino solo la fecha.
|
||||
fn format_birth_brief(birth: &tahuantinsuyu_model::StoredBirthData) -> String {
|
||||
@@ -780,30 +868,10 @@ impl Render for Shell {
|
||||
.child(div().flex_grow())
|
||||
.child(theme_switcher(cx));
|
||||
|
||||
let tree_panel = div()
|
||||
.w(px(TREE_WIDTH))
|
||||
.min_w(px(TREE_WIDTH))
|
||||
.h_full()
|
||||
.border_r_1()
|
||||
.border_color(theme.border)
|
||||
.child(self.tree.clone());
|
||||
|
||||
let canvas_panel = div().flex_grow().h_full().child(self.canvas.clone());
|
||||
|
||||
let main_row = div()
|
||||
let body = div()
|
||||
.flex_grow()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.child(tree_panel)
|
||||
.child(canvas_panel);
|
||||
|
||||
let bottom_panel = div()
|
||||
.h(px(PANEL_HEIGHT))
|
||||
.min_h(px(PANEL_HEIGHT))
|
||||
.w_full()
|
||||
.border_t_1()
|
||||
.border_color(theme.border)
|
||||
.child(self.panel.clone());
|
||||
.child(self.outer_split.clone());
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
@@ -811,7 +879,6 @@ impl Render for Shell {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(header)
|
||||
.child(main_row)
|
||||
.child(bottom_panel)
|
||||
.child(body)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user