tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Definiciones

⎇ 4d14a4495f3a51d7e7682a7f2231d99cb5a3d973 crates/apps/tahuantinsuyu/src/shell.rs Rust · 332 líneas · 11.7 KB Historia Quién tocó qué crudo

feat(tahuantinsuyu): fase 5 — overlay de tránsitos (bi-wheel natal × ahora) 4d14a4495 · 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, compute_with_transits_at_now};
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    /// Overlay de tránsitos al instante actual sobre la natal. Disparado
48    /// por el toggle `show_transits` del panel o la hotkey `[T]`.
49    show_transits: bool,
50}
51
52impl Shell {
53    pub fn new(store: Store, cx: &mut Context<Self>) -> Self {
54        cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
55
56        let bus = cx.new(|_| AppBus);
57        let tree = cx.new(|cx| TahuantinsuyuTree::new(store.clone(), cx));
58        let canvas = cx.new(AstrologyCanvas::new);
59        let panel = cx.new(ControlPanel::new);
60
61        cx.subscribe(&tree, |this: &mut Self, _, ev: &TreeEvent, cx| {
62            this.on_tree_event(ev, cx);
63        })
64        .detach();
65
66        cx.subscribe(&panel, |this: &mut Self, _, ev: &PanelEvent, cx| {
67            this.on_panel_event(ev, cx);
68        })
69        .detach();
70
71        cx.subscribe(&canvas, |this: &mut Self, _, ev: &CanvasEvent, cx| {
72            this.on_canvas_event(ev, cx);
73        })
74        .detach();
75
76        Self {
77            store,
78            bus,
79            tree,
80            canvas,
81            panel,
82            current_chart: None,
83            current_offset_minutes: 0,
84            show_transits: false,
85        }
86    }
87
88    fn on_tree_event(&mut self, ev: &TreeEvent, cx: &mut Context<Self>) {
89        let selection = match ev {
90            TreeEvent::Selected(s) => s,
91            TreeEvent::Opened(s) => s,
92            TreeEvent::HierarchyChanged => {
93                cx.notify();
94                return;
95            }
96        };
97        self.apply_selection(selection.clone(), cx);
98    }
99
100    fn apply_selection(&mut self, sel: TreeSelection, cx: &mut Context<Self>) {
101        match sel {
102            TreeSelection::Chart(id) => {
103                let chart = match self.store.get_chart(id) {
104                    Ok(c) => c,
105                    Err(e) => {
106                        eprintln!("[shell] get_chart {}: {}", id, e);
107                        return;
108                    }
109                };
110                self.current_chart = Some(chart.clone());
111                self.current_offset_minutes = 0;
112                self.render_current(cx);
113                self.panel
114                    .update(cx, |p, cx| p.set_active_kind(Some(chart.kind), cx));
115            }
116            TreeSelection::Contact(id) => {
117                self.current_chart = None;
118                self.current_offset_minutes = 0;
119                let charts = self.store.list_charts(id).unwrap_or_default();
120                let items: Vec<ThumbnailItem> = charts
121                    .into_iter()
122                    .map(|c| ThumbnailItem {
123                        chart_id: c.id,
124                        label: SharedString::from(c.label),
125                        subtitle: Some(SharedString::from(format!("{:?}", c.kind))),
126                        preview: None,
127                    })
128                    .collect();
129                self.canvas.update(cx, |c, cx| {
130                    c.set_mode(
131                        CanvasMode::Thumbnails {
132                            scope: ThumbnailScope::Contact(id),
133                            items,
134                        },
135                        cx,
136                    );
137                });
138                self.panel.update(cx, |p, cx| p.set_active_kind(None, cx));
139            }
140            TreeSelection::Group(id) => {
141                self.current_chart = None;
142                self.current_offset_minutes = 0;
143                let charts = self.store.charts_under_group(id).unwrap_or_default();
144                let items: Vec<ThumbnailItem> = charts
145                    .into_iter()
146                    .map(|c| ThumbnailItem {
147                        chart_id: c.id,
148                        label: SharedString::from(c.label),
149                        subtitle: Some(SharedString::from(format!("{:?}", c.kind))),
150                        preview: None,
151                    })
152                    .collect();
153                self.canvas.update(cx, |c, cx| {
154                    c.set_mode(
155                        CanvasMode::Thumbnails {
156                            scope: ThumbnailScope::Group(id),
157                            items,
158                        },
159                        cx,
160                    );
161                });
162                self.panel.update(cx, |p, cx| p.set_active_kind(None, cx));
163            }
164        }
165    }
166
167    fn render_current(&mut self, cx: &mut Context<Self>) {
168        let Some(chart) = self.current_chart.as_ref() else {
169            return;
170        };
171        let result = if self.show_transits {
172            compute_with_transits_at_now(chart, self.current_offset_minutes)
173        } else {
174            compute_at_offset(chart, self.current_offset_minutes)
175        };
176        let render = match result {
177            Ok(r) => r,
178            Err(e) => {
179                eprintln!(
180                    "[shell] compute {}{} (+{}min): {}",
181                    chart.id,
182                    if self.show_transits { " +transits" } else { "" },
183                    self.current_offset_minutes,
184                    e
185                );
186                return;
187            }
188        };
189        self.canvas.update(cx, |c, cx| {
190            c.set_mode(
191                CanvasMode::Wheel {
192                    render: Box::new(render),
193                },
194                cx,
195            );
196        });
197    }
198
199    fn on_canvas_event(&mut self, ev: &CanvasEvent, cx: &mut Context<Self>) {
200        match ev {
201            CanvasEvent::TimeOffsetChanged(off) => {
202                self.current_offset_minutes = *off;
203                if self.current_chart.is_some() {
204                    self.render_current(cx);
205                }
206            }
207            CanvasEvent::LayerVisibilityChanged { kind, visible } => {
208                // El toggle de Outer (hotkey [T]) significa "transit
209                // overlay" — no es solo un layer hide, dispara un
210                // recompute distinto. El resto son visibility puros.
211                if matches!(kind, LayerKind::Outer) {
212                    self.show_transits = *visible;
213                    self.panel.update(cx, |p, cx| {
214                        p.set_toggle("natal", "show_transits", *visible, cx)
215                    });
216                    self.render_current(cx);
217                    return;
218                }
219                let key = match kind {
220                    LayerKind::SignDial => "show_sign_dial",
221                    LayerKind::Houses => "show_houses",
222                    LayerKind::Aspects => "show_aspects",
223                    LayerKind::Bodies => "show_bodies",
224                    _ => return,
225                };
226                self.panel
227                    .update(cx, |p, cx| p.set_toggle("natal", key, *visible, cx));
228            }
229            CanvasEvent::ChartRequested(_) => {
230                // Fase 5: doble click sobre un thumbnail abre la carta.
231            }
232        }
233    }
234
235    fn on_panel_event(&mut self, ev: &PanelEvent, cx: &mut Context<Self>) {
236        match ev {
237            PanelEvent::ControlChanged {
238                module_id, key, value,
239            } => {
240                let visible = value.as_bool().unwrap_or(true);
241                if module_id == "natal" {
242                    if key == "show_transits" {
243                        self.show_transits = visible;
244                        self.canvas.update(cx, |c, cx| {
245                            c.set_layer_visible(LayerKind::Outer, visible, cx)
246                        });
247                        self.render_current(cx);
248                        return;
249                    }
250                    let kind = match key.as_str() {
251                        "show_sign_dial" => Some(LayerKind::SignDial),
252                        "show_houses" => Some(LayerKind::Houses),
253                        "show_aspects" => Some(LayerKind::Aspects),
254                        "show_bodies" => Some(LayerKind::Bodies),
255                        _ => None,
256                    };
257                    if let Some(k) = kind {
258                        self.canvas
259                            .update(cx, |c, cx| c.set_layer_visible(k, visible, cx));
260                    }
261                }
262            }
263            PanelEvent::ModuleToggled { .. } => {
264                // Fase 6: encender/apagar módulos enteros (Progression,
265                // Synastry, Uranian).
266            }
267        }
268        let _ = (&self.store, &self.tree, &self.bus);
269    }
270}
271
272impl Render for Shell {
273    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
274        let theme = Theme::global(cx).clone();
275
276        let header = div()
277            .h(px(34.0))
278            .px(px(12.0))
279            .flex()
280            .flex_row()
281            .items_center()
282            .gap(px(10.0))
283            .border_b_1()
284            .border_color(theme.border)
285            .child(
286                div()
287                    .text_size(px(13.0))
288                    .text_color(theme.fg_text)
289                    .child("☉ Tahuantinsuyu"),
290            )
291            .child(
292                div()
293                    .text_size(px(10.0))
294                    .text_color(theme.fg_muted)
295                    .child("estudio de astrología profesional"),
296            );
297
298        let tree_panel = div()
299            .w(px(TREE_WIDTH))
300            .min_w(px(TREE_WIDTH))
301            .h_full()
302            .border_r_1()
303            .border_color(theme.border)
304            .child(self.tree.clone());
305
306        let canvas_panel = div().flex_grow().h_full().child(self.canvas.clone());
307
308        let main_row = div()
309            .flex_grow()
310            .flex()
311            .flex_row()
312            .child(tree_panel)
313            .child(canvas_panel);
314
315        let bottom_panel = div()
316            .h(px(PANEL_HEIGHT))
317            .min_h(px(PANEL_HEIGHT))
318            .w_full()
319            .border_t_1()
320            .border_color(theme.border)
321            .child(self.panel.clone());
322
323        div()
324            .size_full()
325            .bg(theme.bg_app.clone())
326            .flex()
327            .flex_col()
328            .child(header)
329            .child(main_row)
330            .child(bottom_panel)
331    }
332}