tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Definitions

⎇ cabdb2927ee6c466b9c338ccd75818f0c436d7f6 crates/apps/tahuantinsuyu/src/shell.rs Rust · 654 lines · 25.7 KB History Who touched what raw

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