⎇ 4d14a4495f3a51d7e7682a7f2231d99cb5a3d973
crates/apps/tahuantinsuyu/src/shell.rs
Historia
Quién tocó qué
crudo
feat(tahuantinsuyu): fase 5 — overlay de tránsitos (bi-wheel natal × ahora)
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}