tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Sutichaykuna

⎇ 0ae622550d62cf0f7dda779729f6a55d312c85f1 crates/apps/tahuantinsuyu/src/shell.rs Rust · 695 siq'ikuna · 27.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(tahuantinsuyu): fase 17 — filtros de aspectos + editor + cleanup + labels 0ae622550 · sergio ·

1//! Shell — coordinador de los tres widgets.
2//!
3//! Es el "director de orquesta": dueño del tree, del canvas y del panel,
4//! reenvía eventos entre ellos y aplica las mutaciones en la store.
5//!
6//! Flujo:
7//!
8//! ```text
9//!   Tree.Selected(Chart)        →  Shell  →  load chart + compose + set_mode(Wheel)
10//!   Tree.Selected(Group/Contact)→  Shell  →  charts_under_*  + set_mode(Thumbnails)
11//!   Canvas.TimeOffsetChanged    →  Shell  →  compose(current_chart, off, requests)
12//!   Canvas.LayerVisibility[T]   →  Shell  →  flip module_configs[transit][enabled]
13//!   Panel.ControlChanged        →  Shell  →  update module_configs OR canvas visibility
14//! ```
15//!
16//! ## module_configs
17//!
18//! Mapa `module_id → JSON` con la configuración persistente de cada
19//! módulo (transit, progression, …). De ahí derivamos los
20//! `PipelineRequest` que la engine consume. Los toggles "visuales"
21//! del NatalModule (`show_sign_dial`, `show_houses`, …) NO viven acá
22//! — afectan solo el render del canvas, no la composición.
23
24use std::collections::HashMap;
25
26use gpui::{
27    Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, Window, div,
28    prelude::*, px,
29};
30
31use tahuantinsuyu_canvas::{
32    AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope,
33};
34use tahuantinsuyu_engine::{
35    LayerKind, NatalOptions, OUTER_RING_MODULES, PipelineRequest, compose_with_options,
36};
37use tahuantinsuyu_model::{Chart, ChartId, ModuleState, TreeSelection};
38use tahuantinsuyu_panel::{ChartOption, ControlPanel, PanelEvent};
39use tahuantinsuyu_store::Store;
40use tahuantinsuyu_tree::{TahuantinsuyuTree, TreeEvent};
41use yahweh_bus::AppBus;
42use yahweh_theme::Theme;
43
44const TREE_WIDTH: f32 = 280.0;
45const PANEL_HEIGHT: f32 = 180.0;
46
47pub struct Shell {
48    store: Store,
49    #[allow(dead_code)]
50    bus: Entity<AppBus>,
51    tree: Entity<TahuantinsuyuTree>,
52    canvas: Entity<AstrologyCanvas>,
53    panel: Entity<ControlPanel>,
54    current_chart: Option<Chart>,
55    current_offset_minutes: i64,
56    /// Estado de los módulos overlay (transit, progression, …) por
57    /// `module_id`. Las claves dentro del JSON dependen del módulo (la
58    /// convención es `"enabled": bool` para el toggle principal).
59    module_configs: HashMap<String, serde_json::Value>,
60}
61
62impl Shell {
63    pub fn new(store: Store, cx: &mut Context<Self>) -> Self {
64        cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
65
66        let bus = cx.new(|_| AppBus);
67        let tree = cx.new(|cx| TahuantinsuyuTree::new(store.clone(), cx));
68        let canvas = cx.new(AstrologyCanvas::new);
69        let panel = cx.new(ControlPanel::new);
70
71        cx.subscribe(&tree, |this: &mut Self, _, ev: &TreeEvent, cx| {
72            this.on_tree_event(ev, cx);
73        })
74        .detach();
75
76        cx.subscribe(&panel, |this: &mut Self, _, ev: &PanelEvent, cx| {
77            this.on_panel_event(ev, cx);
78        })
79        .detach();
80
81        cx.subscribe(&canvas, |this: &mut Self, _, ev: &CanvasEvent, cx| {
82            this.on_canvas_event(ev, cx);
83        })
84        .detach();
85
86        let mut shell = Self {
87            store,
88            bus,
89            tree,
90            canvas,
91            panel,
92            current_chart: None,
93            current_offset_minutes: 0,
94            module_configs: HashMap::new(),
95        };
96        shell.refresh_chart_options(cx);
97        shell
98    }
99
100    /// Recarga la lista de opciones para los `Control::ChartPicker` y
101    /// la pushea al panel. Llamado al boot + tras cada
102    /// `TreeEvent::HierarchyChanged`.
103    fn refresh_chart_options(&self, cx: &mut Context<Self>) {
104        let charts = self.store.list_all_charts().unwrap_or_default();
105        let options: Vec<ChartOption> = charts
106            .into_iter()
107            .map(|c| ChartOption {
108                id: c.id.to_string(),
109                label: format!("{} — {}", c.label, format_birth_brief(&c.birth_data)),
110            })
111            .collect();
112        self.panel
113            .update(cx, |p, cx| p.set_chart_options(options, cx));
114    }
115
116    fn on_tree_event(&mut self, ev: &TreeEvent, cx: &mut Context<Self>) {
117        let selection = match ev {
118            TreeEvent::Selected(s) => s,
119            TreeEvent::Opened(s) => s,
120            TreeEvent::HierarchyChanged => {
121                // La jerarquía cambió (alta/baja de cartas) — refrescar
122                // las opciones del picker para que aparezcan / desaparezcan
123                // en el dropdown.
124                self.refresh_chart_options(cx);
125                cx.notify();
126                return;
127            }
128        };
129        self.apply_selection(selection.clone(), cx);
130    }
131
132    fn apply_selection(&mut self, sel: TreeSelection, cx: &mut Context<Self>) {
133        match sel {
134            TreeSelection::Chart(id) => {
135                let chart = match self.store.get_chart(id) {
136                    Ok(c) => c,
137                    Err(e) => {
138                        eprintln!("[shell] get_chart {}: {}", id, e);
139                        return;
140                    }
141                };
142                let age = current_age_years(&chart.birth_data);
143                self.current_chart = Some(chart.clone());
144                self.current_offset_minutes = 0;
145                // 1) Defaults frescos para esta carta: edad objetivo =
146                //    edad actual. Estos quedan en module_configs como
147                //    valor base si el usuario nunca tocó el slider.
148                self.module_configs.clear();
149                for module_id in ["progression", "solar_arc", "planetary_return"] {
150                    let entry = self
151                        .module_configs
152                        .entry(module_id.into())
153                        .or_insert_with(|| serde_json::json!({}));
154                    if let serde_json::Value::Object(map) = entry {
155                        map.insert("target_age_years".into(), serde_json::json!(age));
156                    }
157                }
158                // El módulo planetary_return además necesita un body
159                // por default — el shell elige "sun" si el usuario no
160                // tocó el Select. La persistencia luego puede pisar
161                // este valor.
162                if let Some(serde_json::Value::Object(map)) =
163                    self.module_configs.get_mut("planetary_return")
164                {
165                    map.entry(String::from("body"))
166                        .or_insert(serde_json::json!("sun"));
167                }
168                // 2) Sobreescribir con lo que el usuario persistió la
169                //    última vez para esta carta (SQLite `module_state`).
170                self.load_persisted_module_states(chart.id);
171                // 3) Sincronizar panel: active_kind + toggles/sliders.
172                self.panel.update(cx, |p, cx| {
173                    p.set_active_kind(Some(chart.kind), cx);
174                });
175                self.sync_panel_from_configs(cx);
176                self.render_current(cx);
177            }
178            TreeSelection::Contact(id) => {
179                self.current_chart = None;
180                self.current_offset_minutes = 0;
181                let charts = self.store.list_charts(id).unwrap_or_default();
182                let items: Vec<ThumbnailItem> = charts
183                    .into_iter()
184                    .map(|c| ThumbnailItem {
185                        chart_id: c.id,
186                        label: SharedString::from(c.label),
187                        subtitle: Some(SharedString::from(format!("{:?}", c.kind))),
188                        preview: None,
189                    })
190                    .collect();
191                self.canvas.update(cx, |c, cx| {
192                    c.set_mode(
193                        CanvasMode::Thumbnails {
194                            scope: ThumbnailScope::Contact(id),
195                            items,
196                        },
197                        cx,
198                    );
199                });
200                self.panel.update(cx, |p, cx| p.set_active_kind(None, cx));
201            }
202            TreeSelection::Group(id) => {
203                self.current_chart = None;
204                self.current_offset_minutes = 0;
205                let charts = self.store.charts_under_group(id).unwrap_or_default();
206                let items: Vec<ThumbnailItem> = charts
207                    .into_iter()
208                    .map(|c| ThumbnailItem {
209                        chart_id: c.id,
210                        label: SharedString::from(c.label),
211                        subtitle: Some(SharedString::from(format!("{:?}", c.kind))),
212                        preview: None,
213                    })
214                    .collect();
215                self.canvas.update(cx, |c, cx| {
216                    c.set_mode(
217                        CanvasMode::Thumbnails {
218                            scope: ThumbnailScope::Group(id),
219                            items,
220                        },
221                        cx,
222                    );
223                });
224                self.panel.update(cx, |p, cx| p.set_active_kind(None, cx));
225            }
226        }
227    }
228
229    /// Deriva los `PipelineRequest` activos a partir del `module_configs`.
230    fn build_requests(&self) -> Vec<PipelineRequest> {
231        let mut requests = Vec::new();
232        if module_enabled(&self.module_configs, "transit") {
233            requests.push(PipelineRequest::Transit);
234        }
235        if module_enabled(&self.module_configs, "progression") {
236            let age = self.module_age_or_current("progression");
237            requests.push(PipelineRequest::SecondaryProgression {
238                target_age_years: age,
239            });
240        }
241        if module_enabled(&self.module_configs, "solar_arc") {
242            let age = self.module_age_or_current("solar_arc");
243            requests.push(PipelineRequest::SolarArc {
244                target_age_years: age,
245            });
246        }
247        if module_enabled(&self.module_configs, "synastry") {
248            if let Some(partner) = self.resolve_synastry_partner() {
249                requests.push(PipelineRequest::Synastry {
250                    partner_chart: Box::new(partner),
251                });
252            }
253        }
254        if module_enabled(&self.module_configs, "planetary_return") {
255            let age = self.module_age_or_current("planetary_return");
256            let body = self
257                .module_configs
258                .get("planetary_return")
259                .and_then(|c| c.get("body"))
260                .and_then(|v| v.as_str())
261                .unwrap_or("sun")
262                .to_string();
263            requests.push(PipelineRequest::PlanetaryReturn {
264                body,
265                target_age_years: age,
266            });
267        }
268        requests
269    }
270
271    /// Resuelve la carta partner para sinastría: 1) si el picker tiene
272    /// un `partner_chart_id` válido en `module_configs`, lo usa; 2)
273    /// si no, cae al automático (primera carta hermana del contacto
274    /// actual). `None` si nada matchea — el request se salta.
275    fn resolve_synastry_partner(&self) -> Option<Chart> {
276        let manual = self
277            .module_configs
278            .get("synastry")
279            .and_then(|c| c.get("partner_chart_id"))
280            .and_then(|v| v.as_str())
281            .and_then(|s| s.parse::<tahuantinsuyu_model::ChartId>().ok())
282            .and_then(|id| self.store.get_chart(id).ok());
283        manual.or_else(|| self.find_synastry_partner_auto())
284    }
285
286    fn find_synastry_partner_auto(&self) -> Option<Chart> {
287        let current = self.current_chart.as_ref()?;
288        let siblings = self.store.list_charts(current.contact_id).ok()?;
289        siblings.into_iter().find(|c| c.id != current.id)
290    }
291
292    /// Deriva las `NatalOptions` activas a partir del `module_configs["natal"]`.
293    /// Si la entry no existe, devuelve defaults (majors=true, minors=false,
294    /// multiplier=1.0).
295    fn build_natal_options(&self) -> NatalOptions {
296        let cfg = self.module_configs.get("natal");
297        let read_bool = |key: &str, default: bool| -> bool {
298            cfg.and_then(|c| c.get(key))
299                .and_then(|v| v.as_bool())
300                .unwrap_or(default)
301        };
302        let read_f64 = |key: &str, default: f64| -> f64 {
303            cfg.and_then(|c| c.get(key))
304                .and_then(|v| v.as_f64())
305                .unwrap_or(default)
306        };
307        NatalOptions {
308            show_majors: read_bool("aspect_majors", true),
309            show_minors: read_bool("aspect_minors", false),
310            orb_multiplier: read_f64("orb_multiplier", 1.0),
311        }
312    }
313
314    /// Lee `module_state` desde SQLite para la carta dada y los mergea
315    /// con los defaults ya cargados en `module_configs`. Los valores
316    /// persistidos ganan sobre los defaults.
317    fn load_persisted_module_states(&mut self, chart_id: ChartId) {
318        let states = match self.store.list_module_states(chart_id) {
319            Ok(s) => s,
320            Err(e) => {
321                eprintln!("[shell] list_module_states {}: {}", chart_id, e);
322                return;
323            }
324        };
325        for st in states {
326            // Re-mergeamos `enabled` (columna separada en SQL) dentro
327            // del JSON config, así el resto del shell sigue leyendo
328            // todo desde una única estructura.
329            let mut combined = match st.config {
330                serde_json::Value::Object(m) => serde_json::Value::Object(m),
331                _ => serde_json::json!({}),
332            };
333            if let serde_json::Value::Object(map) = &mut combined {
334                map.insert("enabled".into(), serde_json::Value::Bool(st.enabled));
335            }
336            // Mergear sobre defaults previos (no sobreescribir si la
337            // entrada nueva no trae un campo).
338            match self.module_configs.entry(st.module_id) {
339                std::collections::hash_map::Entry::Vacant(v) => {
340                    v.insert(combined);
341                }
342                std::collections::hash_map::Entry::Occupied(mut o) => {
343                    if let (serde_json::Value::Object(dst), serde_json::Value::Object(src)) =
344                        (o.get_mut(), &combined)
345                    {
346                        for (k, v) in src {
347                            dst.insert(k.clone(), v.clone());
348                        }
349                    } else {
350                        o.insert(combined);
351                    }
352                }
353            }
354        }
355    }
356
357    /// Pushea cada toggle/slider/picker del `module_configs` al panel
358    /// para que la UI refleje el estado persistido al cargar una carta.
359    fn sync_panel_from_configs(&mut self, cx: &mut Context<Self>) {
360        let snapshot: Vec<(String, serde_json::Value)> = self
361            .module_configs
362            .iter()
363            .map(|(k, v)| (k.clone(), v.clone()))
364            .collect();
365        self.panel.update(cx, |p, cx| {
366            for (module_id, config) in &snapshot {
367                if let serde_json::Value::Object(map) = config {
368                    for (key, value) in map {
369                        if let Some(b) = value.as_bool() {
370                            p.set_toggle(module_id, key, b, cx);
371                        } else if let Some(f) = value.as_f64() {
372                            p.set_slider(module_id, key, f, cx);
373                        } else if let Some(s) = value.as_str() {
374                            p.set_string(module_id, key, Some(s.to_string()), cx);
375                        } else if value.is_null() {
376                            p.set_string(module_id, key, None, cx);
377                        }
378                    }
379                }
380            }
381        });
382    }
383
384    /// Persiste el estado actual de un módulo a SQLite. Extrae
385    /// `enabled` del JSON y lo guarda en la columna dedicada; el resto
386    /// va al `config_json`.
387    fn persist_module(&self, module_id: &str) {
388        let Some(chart) = self.current_chart.as_ref() else {
389            return;
390        };
391        let Some(config) = self.module_configs.get(module_id) else {
392            return;
393        };
394        let enabled = config
395            .get("enabled")
396            .and_then(|v| v.as_bool())
397            .unwrap_or(false);
398        let mut clean = config.clone();
399        if let serde_json::Value::Object(map) = &mut clean {
400            map.remove("enabled");
401        }
402        let state = ModuleState {
403            chart_id: chart.id,
404            module_id: module_id.to_string(),
405            enabled,
406            config: clean,
407        };
408        if let Err(e) = self.store.upsert_module_state(&state) {
409            eprintln!("[shell] upsert_module_state {}: {}", module_id, e);
410        }
411    }
412
413    /// Lee `target_age_years` del módulo o cae a la edad actual del
414    /// sujeto (calculada desde la fecha de nacimiento y el reloj).
415    fn module_age_or_current(&self, module_id: &str) -> f64 {
416        self.module_configs
417            .get(module_id)
418            .and_then(|c| c.get("target_age_years"))
419            .and_then(|v| v.as_f64())
420            .unwrap_or_else(|| {
421                self.current_chart
422                    .as_ref()
423                    .map(|c| current_age_years(&c.birth_data))
424                    .unwrap_or(0.0)
425            })
426    }
427
428    fn render_current(&mut self, cx: &mut Context<Self>) {
429        let Some(chart) = self.current_chart.as_ref() else {
430            return;
431        };
432        let requests = self.build_requests();
433        let natal_options = self.build_natal_options();
434        let render = match compose_with_options(
435            chart,
436            self.current_offset_minutes,
437            &requests,
438            &natal_options,
439        ) {
440            Ok(r) => r,
441            Err(e) => {
442                eprintln!(
443                    "[shell] compose {} (+{}min, {} reqs): {}",
444                    chart.id,
445                    self.current_offset_minutes,
446                    requests.len(),
447                    e
448                );
449                return;
450            }
451        };
452        self.canvas.update(cx, |c, cx| {
453            c.set_mode(
454                CanvasMode::Wheel {
455                    render: Box::new(render),
456                },
457                cx,
458            );
459        });
460    }
461
462    fn on_canvas_event(&mut self, ev: &CanvasEvent, cx: &mut Context<Self>) {
463        match ev {
464            CanvasEvent::TimeOffsetChanged(off) => {
465                self.current_offset_minutes = *off;
466                if self.current_chart.is_some() {
467                    self.render_current(cx);
468                }
469            }
470            CanvasEvent::LayerVisibilityChanged { kind, visible } => {
471                // El toggle de Outer ([T]) no es visibility puro: dispara
472                // un pipeline distinto. Lo traducimos a un cambio en
473                // module_configs["transit"]["enabled"] + re-render.
474                if matches!(kind, LayerKind::Outer) {
475                    set_module_enabled(&mut self.module_configs, "transit", *visible);
476                    self.persist_module("transit");
477                    self.panel.update(cx, |p, cx| {
478                        p.set_toggle("transit", "enabled", *visible, cx)
479                    });
480                    self.render_current(cx);
481                    return;
482                }
483                // El resto son visibility puros sobre el canvas. Sync el
484                // panel para que el toggle visual coincida con la hotkey.
485                let key = match kind {
486                    LayerKind::SignDial => "show_sign_dial",
487                    LayerKind::Houses => "show_houses",
488                    LayerKind::Aspects => "show_aspects",
489                    LayerKind::Bodies => "show_bodies",
490                    _ => return,
491                };
492                self.panel
493                    .update(cx, |p, cx| p.set_toggle("natal", key, *visible, cx));
494            }
495            CanvasEvent::ChartRequested(_) => {
496                // Fase 7: doble click sobre un thumbnail abre la carta.
497            }
498        }
499    }
500
501    fn on_panel_event(&mut self, ev: &PanelEvent, cx: &mut Context<Self>) {
502        match ev {
503            PanelEvent::ControlChanged {
504                module_id, key, value,
505            } => {
506                let bool_val = value.as_bool().unwrap_or(true);
507                if module_id == "natal" {
508                    // Distinguimos: show_* = visibility (no recompose),
509                    // aspect_*/orb_* = filtros de engine (recompose +
510                    // persist).
511                    let kind = match key.as_str() {
512                        "show_sign_dial" => Some(LayerKind::SignDial),
513                        "show_houses" => Some(LayerKind::Houses),
514                        "show_aspects" => Some(LayerKind::Aspects),
515                        "show_bodies" => Some(LayerKind::Bodies),
516                        _ => None,
517                    };
518                    if let Some(k) = kind {
519                        self.canvas
520                            .update(cx, |c, cx| c.set_layer_visible(k, bool_val, cx));
521                    } else {
522                        // Filtros: actualizar module_configs + recompose.
523                        let entry = self
524                            .module_configs
525                            .entry("natal".into())
526                            .or_insert_with(|| serde_json::json!({}));
527                        if let serde_json::Value::Object(map) = entry {
528                            map.insert(key.clone(), value.clone());
529                        }
530                        self.persist_module("natal");
531                        self.render_current(cx);
532                    }
533                } else {
534                    // Cualquier otro módulo: actualizamos su config y
535                    // recompomemos. La engine vuelve a llamarse con el
536                    // PipelineRequest derivado del nuevo estado.
537                    let entry = self
538                        .module_configs
539                        .entry(module_id.clone())
540                        .or_insert_with(|| serde_json::json!({}));
541                    if let serde_json::Value::Object(map) = entry {
542                        map.insert(key.clone(), value.clone());
543                    }
544                    // Transit, Synastry y Solar Return comparten el
545                    // outer ring del canvas — son mutuamente excluyentes.
546                    // Al prender uno, apagamos los otros + sync panel +
547                    // persist.
548                    if key == "enabled" && bool_val && OUTER_RING_MODULES.contains(&module_id.as_str()) {
549                        for &other in OUTER_RING_MODULES.iter() {
550                            if other != module_id && module_enabled(&self.module_configs, other) {
551                                set_module_enabled(&mut self.module_configs, other, false);
552                                let other_str = other.to_string();
553                                self.panel.update(cx, |p, cx| {
554                                    p.set_toggle(&other_str, "enabled", false, cx)
555                                });
556                                self.persist_module(&other_str);
557                            }
558                        }
559                    }
560                    // Sincronizar visualmente el toggle [T] del canvas
561                    // cuando el cambio afecta el outer ring (transit,
562                    // synastry o solar_return).
563                    if OUTER_RING_MODULES.contains(&module_id.as_str()) && key == "enabled" {
564                        self.canvas.update(cx, |c, cx| {
565                            c.set_layer_visible(LayerKind::Outer, bool_val, cx)
566                        });
567                    }
568                    self.persist_module(module_id);
569                    self.render_current(cx);
570                }
571            }
572            PanelEvent::ModuleToggled { .. } => {
573                // Fase 7: encender/apagar módulos enteros desde un
574                // header con switch (vs. el toggle por-control de hoy).
575            }
576        }
577        let _ = (&self.store, &self.tree, &self.bus);
578    }
579}
580
581// =====================================================================
582// Helpers de module_configs
583// =====================================================================
584
585// OUTER_RING_MODULES viene de tahuantinsuyu_engine — single source of
586// truth. Shell y canvas leen del mismo slice.
587
588
589/// Etiqueta breve para mostrar al elegir una carta en el picker:
590/// `"YYYY-MM-DD · Lugar"` cuando hay lugar, sino solo la fecha.
591fn format_birth_brief(birth: &tahuantinsuyu_model::StoredBirthData) -> String {
592    let date = format!("{:04}-{:02}-{:02}", birth.year, birth.month, birth.day);
593    match &birth.birthplace_label {
594        Some(p) if !p.is_empty() => format!("{} · {}", date, p),
595        _ => date,
596    }
597}
598
599/// Edad en años decimales desde el nacimiento hasta el reloj actual.
600/// Aproximación: ignora la TZ de nacimiento (no afecta a resolución de
601/// año) y usa una fracción de año tropical sobre los segundos Unix.
602fn current_age_years(birth: &tahuantinsuyu_model::StoredBirthData) -> f64 {
603    use std::time::{SystemTime, UNIX_EPOCH};
604    let now_secs = SystemTime::now()
605        .duration_since(UNIX_EPOCH)
606        .map(|d| d.as_secs_f64())
607        .unwrap_or(0.0);
608    let birth_year_frac = birth.year as f64
609        + (birth.month.saturating_sub(1) as f64) / 12.0
610        + (birth.day.saturating_sub(1) as f64) / 365.25;
611    let now_year_frac = 1970.0 + now_secs / (365.2422 * 86400.0);
612    (now_year_frac - birth_year_frac).max(0.0)
613}
614
615fn module_enabled(cfgs: &HashMap<String, serde_json::Value>, id: &str) -> bool {
616    cfgs.get(id)
617        .and_then(|c| c.get("enabled"))
618        .and_then(|v| v.as_bool())
619        .unwrap_or(false)
620}
621
622fn set_module_enabled(
623    cfgs: &mut HashMap<String, serde_json::Value>,
624    id: &str,
625    enabled: bool,
626) {
627    let entry = cfgs
628        .entry(id.to_string())
629        .or_insert_with(|| serde_json::json!({}));
630    if let serde_json::Value::Object(map) = entry {
631        map.insert("enabled".into(), serde_json::Value::Bool(enabled));
632    }
633}
634
635impl Render for Shell {
636    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
637        let theme = Theme::global(cx).clone();
638
639        let header = div()
640            .h(px(34.0))
641            .px(px(12.0))
642            .flex()
643            .flex_row()
644            .items_center()
645            .gap(px(10.0))
646            .border_b_1()
647            .border_color(theme.border)
648            .child(
649                div()
650                    .text_size(px(13.0))
651                    .text_color(theme.fg_text)
652                    .child("☉ Tahuantinsuyu"),
653            )
654            .child(
655                div()
656                    .text_size(px(10.0))
657                    .text_color(theme.fg_muted)
658                    .child("estudio de astrología profesional"),
659            );
660
661        let tree_panel = div()
662            .w(px(TREE_WIDTH))
663            .min_w(px(TREE_WIDTH))
664            .h_full()
665            .border_r_1()
666            .border_color(theme.border)
667            .child(self.tree.clone());
668
669        let canvas_panel = div().flex_grow().h_full().child(self.canvas.clone());
670
671        let main_row = div()
672            .flex_grow()
673            .flex()
674            .flex_row()
675            .child(tree_panel)
676            .child(canvas_panel);
677
678        let bottom_panel = div()
679            .h(px(PANEL_HEIGHT))
680            .min_h(px(PANEL_HEIGHT))
681            .w_full()
682            .border_t_1()
683            .border_color(theme.border)
684            .child(self.panel.clone());
685
686        div()
687            .size_full()
688            .bg(theme.bg_app.clone())
689            .flex()
690            .flex_col()
691            .child(header)
692            .child(main_row)
693            .child(bottom_panel)
694    }
695}