⎇ 360797132ef9bc5e4d7f5056d4885be737d3dc39
crates/apps/tahuantinsuyu/src/shell.rs
Historia
Quién tocó qué
crudo
feat(tahuantinsuyu): fase 4 — jog-dial perimetral, hotkeys y panel interactivo
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}