tawasuyu

tawasuyu/brahman

Rust 320TOML 122JSON 14Markdown 114164 Definiciones

⎇ 360797132ef9bc5e4d7f5056d4885be737d3dc39 crates/apps/tahuantinsuyu/src/shell.rs Rust · 301 líneas · 10.4 KB Historia Quién tocó qué crudo

feat(tahuantinsuyu): fase 4 — jog-dial perimetral, hotkeys y panel interactivo 360797132 · 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 + compute + set_mode(Wheel)
10//!   Tree.Selected(Group/Contact)→  Shell  →  charts_under_*  + set_mode(Thumbnails)
11//!   Canvas.TimeOffsetChanged    →  Shell  →  compute_at_offset(current_chart, off)
12//!                                          →  set_mode(Wheel) con la rueda re-pintada
13//!   Canvas.LayerVisibility...   →  Shell  →  Panel.set_toggle (mantener sync visual)
14//!   Panel.ControlChanged        →  Shell  →  Canvas.set_layer_visible (show_*)
15//! ```
16
17use gpui::{
18    Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, Window, div,
19    prelude::*, px,
20};
21
22use tahuantinsuyu_canvas::{
23    AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope,
24};
25use tahuantinsuyu_engine::{LayerKind, compute_at_offset};
26use tahuantinsuyu_model::{Chart, TreeSelection};
27use tahuantinsuyu_panel::{ControlPanel, PanelEvent};
28use tahuantinsuyu_store::Store;
29use tahuantinsuyu_tree::{TahuantinsuyuTree, TreeEvent};
30use yahweh_bus::AppBus;
31use yahweh_theme::Theme;
32
33const TREE_WIDTH: f32 = 280.0;
34const PANEL_HEIGHT: f32 = 180.0;
35
36pub struct Shell {
37    store: Store,
38    #[allow(dead_code)]
39    bus: Entity<AppBus>,
40    tree: Entity<TahuantinsuyuTree>,
41    canvas: Entity<AstrologyCanvas>,
42    panel: Entity<ControlPanel>,
43    /// Carta abierta actualmente en el canvas. La cacheamos para poder
44    /// recomputarla con time-offsets sin re-leer la DB cada vez.
45    current_chart: Option<Chart>,
46    current_offset_minutes: i64,
47}
48
49impl Shell {
50    pub fn new(store: Store, cx: &mut Context<Self>) -> Self {
51        cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
52
53        let bus = cx.new(|_| AppBus);
54        let tree = cx.new(|cx| TahuantinsuyuTree::new(store.clone(), cx));
55        let canvas = cx.new(AstrologyCanvas::new);
56        let panel = cx.new(ControlPanel::new);
57
58        cx.subscribe(&tree, |this: &mut Self, _, ev: &TreeEvent, cx| {
59            this.on_tree_event(ev, cx);
60        })
61        .detach();
62
63        cx.subscribe(&panel, |this: &mut Self, _, ev: &PanelEvent, cx| {
64            this.on_panel_event(ev, cx);
65        })
66        .detach();
67
68        cx.subscribe(&canvas, |this: &mut Self, _, ev: &CanvasEvent, cx| {
69            this.on_canvas_event(ev, cx);
70        })
71        .detach();
72
73        Self {
74            store,
75            bus,
76            tree,
77            canvas,
78            panel,
79            current_chart: None,
80            current_offset_minutes: 0,
81        }
82    }
83
84    fn on_tree_event(&mut self, ev: &TreeEvent, cx: &mut Context<Self>) {
85        let selection = match ev {
86            TreeEvent::Selected(s) => s,
87            TreeEvent::Opened(s) => s,
88            TreeEvent::HierarchyChanged => {
89                cx.notify();
90                return;
91            }
92        };
93        self.apply_selection(selection.clone(), cx);
94    }
95
96    fn apply_selection(&mut self, sel: TreeSelection, cx: &mut Context<Self>) {
97        match sel {
98            TreeSelection::Chart(id) => {
99                let chart = match self.store.get_chart(id) {
100                    Ok(c) => c,
101                    Err(e) => {
102                        eprintln!("[shell] get_chart {}: {}", id, e);
103                        return;
104                    }
105                };
106                self.current_chart = Some(chart.clone());
107                self.current_offset_minutes = 0;
108                self.render_current(cx);
109                self.panel
110                    .update(cx, |p, cx| p.set_active_kind(Some(chart.kind), cx));
111            }
112            TreeSelection::Contact(id) => {
113                self.current_chart = None;
114                self.current_offset_minutes = 0;
115                let charts = self.store.list_charts(id).unwrap_or_default();
116                let items: Vec<ThumbnailItem> = charts
117                    .into_iter()
118                    .map(|c| ThumbnailItem {
119                        chart_id: c.id,
120                        label: SharedString::from(c.label),
121                        subtitle: Some(SharedString::from(format!("{:?}", c.kind))),
122                        preview: None,
123                    })
124                    .collect();
125                self.canvas.update(cx, |c, cx| {
126                    c.set_mode(
127                        CanvasMode::Thumbnails {
128                            scope: ThumbnailScope::Contact(id),
129                            items,
130                        },
131                        cx,
132                    );
133                });
134                self.panel.update(cx, |p, cx| p.set_active_kind(None, cx));
135            }
136            TreeSelection::Group(id) => {
137                self.current_chart = None;
138                self.current_offset_minutes = 0;
139                let charts = self.store.charts_under_group(id).unwrap_or_default();
140                let items: Vec<ThumbnailItem> = charts
141                    .into_iter()
142                    .map(|c| ThumbnailItem {
143                        chart_id: c.id,
144                        label: SharedString::from(c.label),
145                        subtitle: Some(SharedString::from(format!("{:?}", c.kind))),
146                        preview: None,
147                    })
148                    .collect();
149                self.canvas.update(cx, |c, cx| {
150                    c.set_mode(
151                        CanvasMode::Thumbnails {
152                            scope: ThumbnailScope::Group(id),
153                            items,
154                        },
155                        cx,
156                    );
157                });
158                self.panel.update(cx, |p, cx| p.set_active_kind(None, cx));
159            }
160        }
161    }
162
163    fn render_current(&mut self, cx: &mut Context<Self>) {
164        let Some(chart) = self.current_chart.as_ref() else {
165            return;
166        };
167        let render = match compute_at_offset(chart, self.current_offset_minutes) {
168            Ok(r) => r,
169            Err(e) => {
170                eprintln!(
171                    "[shell] compute_at_offset {} (+{}min): {}",
172                    chart.id, self.current_offset_minutes, e
173                );
174                return;
175            }
176        };
177        self.canvas.update(cx, |c, cx| {
178            c.set_mode(
179                CanvasMode::Wheel {
180                    render: Box::new(render),
181                },
182                cx,
183            );
184        });
185    }
186
187    fn on_canvas_event(&mut self, ev: &CanvasEvent, cx: &mut Context<Self>) {
188        match ev {
189            CanvasEvent::TimeOffsetChanged(off) => {
190                self.current_offset_minutes = *off;
191                if self.current_chart.is_some() {
192                    self.render_current(cx);
193                }
194            }
195            CanvasEvent::LayerVisibilityChanged { kind, visible } => {
196                // Sync el panel para que el toggle visual coincida con
197                // lo que disparó el hotkey en el canvas.
198                let key = match kind {
199                    LayerKind::SignDial => "show_sign_dial",
200                    LayerKind::Houses => "show_houses",
201                    LayerKind::Aspects => "show_aspects",
202                    LayerKind::Bodies => "show_bodies",
203                    _ => return,
204                };
205                self.panel
206                    .update(cx, |p, cx| p.set_toggle("natal", key, *visible, cx));
207            }
208            CanvasEvent::ChartRequested(_) => {
209                // Fase 5: doble click sobre un thumbnail abre la carta.
210            }
211        }
212    }
213
214    fn on_panel_event(&mut self, ev: &PanelEvent, cx: &mut Context<Self>) {
215        match ev {
216            PanelEvent::ControlChanged { module_id, key, value } => {
217                let visible = value.as_bool().unwrap_or(true);
218                if module_id == "natal" {
219                    let kind = match key.as_str() {
220                        "show_sign_dial" => Some(LayerKind::SignDial),
221                        "show_houses" => Some(LayerKind::Houses),
222                        "show_aspects" => Some(LayerKind::Aspects),
223                        "show_bodies" => Some(LayerKind::Bodies),
224                        _ => None,
225                    };
226                    if let Some(k) = kind {
227                        self.canvas
228                            .update(cx, |c, cx| c.set_layer_visible(k, visible, cx));
229                    }
230                }
231            }
232            PanelEvent::ModuleToggled { .. } => {
233                // Fase 5: encender/apagar módulos enteros (Transit,
234                // Synastry, Uranian).
235            }
236        }
237        let _ = (&self.store, &self.tree, &self.bus);
238    }
239}
240
241impl Render for Shell {
242    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
243        let theme = Theme::global(cx).clone();
244
245        let header = div()
246            .h(px(34.0))
247            .px(px(12.0))
248            .flex()
249            .flex_row()
250            .items_center()
251            .gap(px(10.0))
252            .border_b_1()
253            .border_color(theme.border)
254            .child(
255                div()
256                    .text_size(px(13.0))
257                    .text_color(theme.fg_text)
258                    .child("☉ Tahuantinsuyu"),
259            )
260            .child(
261                div()
262                    .text_size(px(10.0))
263                    .text_color(theme.fg_muted)
264                    .child("estudio de astrología profesional"),
265            );
266
267        let tree_panel = div()
268            .w(px(TREE_WIDTH))
269            .min_w(px(TREE_WIDTH))
270            .h_full()
271            .border_r_1()
272            .border_color(theme.border)
273            .child(self.tree.clone());
274
275        let canvas_panel = div().flex_grow().h_full().child(self.canvas.clone());
276
277        let main_row = div()
278            .flex_grow()
279            .flex()
280            .flex_row()
281            .child(tree_panel)
282            .child(canvas_panel);
283
284        let bottom_panel = div()
285            .h(px(PANEL_HEIGHT))
286            .min_h(px(PANEL_HEIGHT))
287            .w_full()
288            .border_t_1()
289            .border_color(theme.border)
290            .child(self.panel.clone());
291
292        div()
293            .size_full()
294            .bg(theme.bg_app.clone())
295            .flex()
296            .flex_col()
297            .child(header)
298            .child(main_row)
299            .child(bottom_panel)
300    }
301}