tawasuyu

tawasuyu/brahman

Rust 332TOML 127JSON 17Markdown 154710 Definitions

⎇ 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd crates/apps/yahweh-shell/src/status_panel.rs Rust · 343 lines · 12.1 KB History Who touched what raw

chore: monorepo inicial con arje + minga + yahweh absorbidos 53dbdf0f1 · Sergio ·

1//! `StatusPanel` — panel lateral con info de la app, switcher de tema y
2//! controles de swap del kind del contenedor objetivo (Fase 5).
3//!
4//! Recibe el `Entity<LayoutModel>` para mutarlo: cuando el usuario clickea
5//! "Tabs", el StatusPanel hace
6//! `model.update(cx, |m, cx| m.set_kind(target_id, "Tabs", cx))`. El
7//! LayoutHost está suscripto al model y rebuildea preservando los hijos
8//! del contenedor por NodeId — eso es lo que demuestra que swappear el
9//! contenedor no resetea el contenido (las pestañas activas, expansiones
10//! del FileExplorer, scroll position, todo se mantiene).
11
12use gpui::{
13    ClickEvent, Context, Entity, IntoElement, Render, SharedString, Window, div, prelude::*, px,
14};
15
16use yahweh_core::{LayerConfig, NodeId};
17use yahweh_theme::Theme;
18
19use crate::layout_model::LayoutModel;
20
21/// Id del contenedor que el StatusPanel controla con sus botones de swap.
22/// Hardcoded por ahora; en el futuro lo podríamos leer de un param del
23/// JSON (e.g. `target_id`) para que cualquier StatusPanel apunte al
24/// container que querramos.
25const SWAP_TARGET_ID: &str = "explorers";
26
27const SWAPPABLE_KINDS: &[&str] = &["Split", "Tabs", "Tiled"];
28
29/// Path del JSON que el botón "Reload" relee. Mantenemos consistencia con
30/// `main.rs` — si llegamos a parametrizarlo, hacerlo en un solo lugar.
31const LAYOUT_PATH: &str = "layout.json";
32
33pub struct StatusPanel {
34    model: Entity<LayoutModel>,
35    last_event: SharedString,
36}
37
38impl StatusPanel {
39    pub fn new(model: Entity<LayoutModel>, cx: &mut Context<Self>) -> Self {
40        cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
41        // Subscripción al model para refrescar el "kind activo" del swap.
42        cx.observe(&model, |_, _, cx| cx.notify()).detach();
43        Self {
44            model,
45            last_event: "(esperando bus app-level — Fase 6)".into(),
46        }
47    }
48
49    /// Reservado para Fase 6: el AppBus va a actualizar este texto.
50    #[allow(dead_code)]
51    pub fn set_status(&mut self, text: SharedString, cx: &mut Context<Self>) {
52        self.last_event = text;
53        cx.notify();
54    }
55
56    fn cycle_theme(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
57        let current = Theme::global(cx).name.to_string();
58        Theme::set(cx, Theme::next_after(&current));
59        cx.notify();
60    }
61
62    fn pick_theme(
63        &mut self,
64        name: SharedString,
65        _: &ClickEvent,
66        _: &mut Window,
67        cx: &mut Context<Self>,
68    ) {
69        if let Some(theme) = Theme::by_name(&name) {
70            Theme::set(cx, theme);
71            cx.notify();
72        }
73    }
74
75    fn swap_container(
76        &mut self,
77        kind: SharedString,
78        _: &ClickEvent,
79        _: &mut Window,
80        cx: &mut Context<Self>,
81    ) {
82        let target = NodeId::new(SWAP_TARGET_ID);
83        self.model.update(cx, |m, cx| {
84            m.set_kind(&target, &kind, cx);
85        });
86    }
87
88    fn reload_from_disk(
89        &mut self,
90        _: &ClickEvent,
91        _: &mut Window,
92        cx: &mut Context<Self>,
93    ) {
94        // Releemos el JSON. Si el parsing falla, `load_or_default` cae al
95        // árbol default — no panicamos. La preservación de hijos
96        // funciona vía el id JSON: lo que matchee, persiste; lo nuevo se
97        // instancia.
98        let tree = LayerConfig::load_or_default(LAYOUT_PATH);
99        self.model.update(cx, |m, cx| m.replace_tree(tree, cx));
100    }
101
102    /// Lee el `kind` actual del contenedor objetivo desde el model. Si el
103    /// id no existe (alguien cambió el JSON), devuelve `None` y no
104    /// resaltamos ningún chip.
105    fn current_target_kind(&self, cx: &Context<Self>) -> Option<String> {
106        find_kind(self.model.read(cx).tree(), SWAP_TARGET_ID)
107    }
108}
109
110fn find_kind(node: &yahweh_core::LayerConfig, target: &str) -> Option<String> {
111    if let Some(id) = &node.id {
112        if id == target {
113            return Some(node.kind.clone());
114        }
115    }
116    for child in &node.children {
117        if let Some(k) = find_kind(child, target) {
118            return Some(k);
119        }
120    }
121    None
122}
123
124impl Render for StatusPanel {
125    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
126        let theme = Theme::global(cx).clone();
127        let active_theme = theme.name;
128        let theme_chips: Vec<_> = Theme::all()
129            .into_iter()
130            .map(|t| (t.name, t.name == active_theme))
131            .collect();
132
133        let current_kind = self.current_target_kind(cx);
134
135        // Theme chips.
136        let mut theme_row = div().flex().flex_row().flex_wrap().gap(px(6.0));
137        for (name, is_active) in theme_chips {
138            let bg = if is_active {
139                theme.bg_row_active
140            } else {
141                theme.bg_row_hover
142            };
143            let border = if is_active {
144                theme.accent_strong
145            } else {
146                theme.border
147            };
148            let chip_id = SharedString::from(format!("theme-chip-{}", name));
149            let chip_name = SharedString::from(name.to_string());
150            theme_row = theme_row.child(
151                div()
152                    .id(chip_id)
153                    .px(px(10.0))
154                    .py(px(5.0))
155                    .rounded(px(4.0))
156                    .border_1()
157                    .border_color(border)
158                    .bg(bg)
159                    .text_size(px(11.0))
160                    .text_color(theme.fg_text)
161                    .hover(|s| s.opacity(0.85))
162                    .child(SharedString::from(name.to_string()))
163                    .on_click(cx.listener(move |this, click, w, cx| {
164                        this.pick_theme(chip_name.clone(), click, w, cx);
165                    })),
166            );
167        }
168
169        // Swap chips — uno por kind (Split / Tabs / Tiled). El activo
170        // refleja el `kind` actual del nodo objetivo.
171        let mut swap_row = div().flex().flex_row().gap(px(6.0));
172        for &kind in SWAPPABLE_KINDS {
173            let is_active = current_kind.as_deref() == Some(kind);
174            let bg = if is_active {
175                theme.bg_row_active
176            } else {
177                theme.bg_row_hover
178            };
179            let border = if is_active {
180                theme.accent_strong
181            } else {
182                theme.border
183            };
184            let chip_id = SharedString::from(format!("swap-chip-{}", kind));
185            let kind_str = SharedString::from(kind.to_string());
186            swap_row = swap_row.child(
187                div()
188                    .id(chip_id)
189                    .px(px(12.0))
190                    .py(px(6.0))
191                    .rounded(px(4.0))
192                    .border_1()
193                    .border_color(border)
194                    .bg(bg)
195                    .text_size(px(12.0))
196                    .text_color(theme.fg_text)
197                    .hover(|s| s.opacity(0.85))
198                    .child(SharedString::from(kind.to_string()))
199                    .on_click(cx.listener(move |this, click, w, cx| {
200                        this.swap_container(kind_str.clone(), click, w, cx);
201                    })),
202            );
203        }
204
205        div()
206            .size_full()
207            .bg(theme.bg_panel_alt.clone())
208            .p(px(20.0))
209            .flex()
210            .flex_col()
211            .gap(px(14.0))
212            .child(
213                div()
214                    .text_size(px(22.0))
215                    .text_color(theme.accent_strong)
216                    .child("Yahweh — Fase 5"),
217            )
218            .child(
219                div()
220                    .text_color(theme.fg_muted)
221                    .text_size(px(12.0))
222                    .child("Contenedores intercambiables sin perder hijos."),
223            )
224            // ----- persistencia + reload -----
225            .child(
226                div()
227                    .mt(px(14.0))
228                    .text_color(theme.fg_muted)
229                    .text_size(px(11.0))
230                    .child("layout.json:"),
231            )
232            .child(
233                div()
234                    .flex()
235                    .flex_row()
236                    .items_center()
237                    .gap(px(8.0))
238                    .child(
239                        div()
240                            .text_size(px(10.0))
241                            .text_color(theme.fg_muted)
242                            .child(
243                                "auto-save al swap o al soltar un divisor.",
244                            ),
245                    )
246                    .child(
247                        div()
248                            .id("reload-from-disk")
249                            .px(px(10.0))
250                            .py(px(4.0))
251                            .rounded(px(4.0))
252                            .border_1()
253                            .border_color(theme.border_strong)
254                            .bg(theme.bg_panel.clone())
255                            .text_color(theme.fg_text)
256                            .text_size(px(11.0))
257                            .hover(|s| s.opacity(0.85))
258                            .on_click(cx.listener(Self::reload_from_disk))
259                            .child("⤓ reload"),
260                    ),
261            )
262            // ----- swap del contenedor 'explorers' -----
263            .child(
264                div()
265                    .mt(px(14.0))
266                    .text_color(theme.fg_muted)
267                    .text_size(px(11.0))
268                    .child(SharedString::from(format!(
269                        "contenedor '{}' — kind:",
270                        SWAP_TARGET_ID
271                    ))),
272            )
273            .child(swap_row)
274            .child(
275                div()
276                    .mt(px(2.0))
277                    .text_color(theme.fg_muted)
278                    .text_size(px(10.0))
279                    .child(
280                        "click ⇒ swappea el kind del contenedor padre. Los hijos \
281                         (FileExplorer, DatabaseExplorer) se preservan: cualquier \
282                         folder expandido o entry seleccionado sigue así tras el swap.",
283                    ),
284            )
285            // ----- log de evento (placeholder Fase 6) -----
286            .child(
287                div()
288                    .mt(px(16.0))
289                    .text_color(theme.fg_muted)
290                    .text_size(px(11.0))
291                    .child("último evento (bus Fase 6):"),
292            )
293            .child(
294                div()
295                    .px(px(10.0))
296                    .py(px(8.0))
297                    .bg(theme.bg_panel.clone())
298                    .border_1()
299                    .border_color(theme.border)
300                    .rounded(px(4.0))
301                    .text_size(px(11.0))
302                    .text_color(theme.fg_text)
303                    .child(self.last_event.clone()),
304            )
305            // ----- theme switcher -----
306            .child(
307                div()
308                    .mt(px(20.0))
309                    .text_color(theme.fg_muted)
310                    .text_size(px(11.0))
311                    .child("tema activo:"),
312            )
313            .child(
314                div()
315                    .flex()
316                    .flex_row()
317                    .items_center()
318                    .gap(px(10.0))
319                    .child(
320                        div()
321                            .text_color(theme.accent)
322                            .text_size(px(15.0))
323                            .child(SharedString::from(theme.name.to_string())),
324                    )
325                    .child(
326                        div()
327                            .id("cycle-theme")
328                            .px(px(10.0))
329                            .py(px(4.0))
330                            .rounded(px(4.0))
331                            .border_1()
332                            .border_color(theme.border_strong)
333                            .bg(theme.bg_panel.clone())
334                            .text_color(theme.fg_text)
335                            .text_size(px(11.0))
336                            .hover(|s| s.opacity(0.85))
337                            .on_click(cx.listener(Self::cycle_theme))
338                            .child("⇄ siguiente"),
339                    ),
340            )
341            .child(theme_row)
342    }
343}