gioser

tawasuyu/brahman

Rust 483TOML 191Markdown 50JSON 176777 Definitions

⎇ ee27108f6c9abe63ac3abdca743da82b467fc2ca crates/modules/mirada/mirada-body/src/lib.rs Rust · 445 lines · 15.6 KB History Who touched what raw

feat(mirada): acople del shell — ventana-dock al pie de la pantalla ee27108f6 · sergio ·

1//! `mirada-body` — el estado del Cuerpo del compositor.
2//!
3//! El "Cuerpo" de carmen (`mirada-compositor`, sobre `smithay`) tiene
4//! dos mitades: el *backend*, que habla Wayland y posee el hardware, y
5//! esta *contabilidad* — qué salidas y superficies existen y con qué
6//! geometría. Aislarla deja el backend reducido a "ejecuta estas
7//! [`BodyOp`]" y la hace testeable sin un servidor gráfico.
8//!
9//! El flujo es simétrico al del Cerebro:
10//!
11//! - El backend avisa de cambios de hardware/clientes con los mutadores
12//!   ([`BodyState::open_surface`], [`BodyState::add_output`], …), que
13//!   devuelven el [`BodyEvent`] a mandar al Cerebro.
14//! - El Cerebro responde con [`BrainCommand`]s; [`BodyState::apply`] los
15//!   traduce a [`BodyOp`]s concretas que el backend ejecuta.
16
17#![forbid(unsafe_code)]
18
19use std::collections::BTreeMap;
20
21use mirada_protocol::{BodyEvent, BrainCommand, OutputId, Rect, WindowId};
22
23/// Una superficie Wayland desde la óptica del Cuerpo.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Surface {
26    pub app_id: String,
27    pub title: String,
28    /// Geometría aplicada — `None` hasta la primera [`BodyOp::Configure`].
29    pub geometry: Option<Rect>,
30    pub visible: bool,
31    pub focused: bool,
32    /// `true` si flota: el backend la pinta por encima de las teseladas.
33    pub floating: bool,
34    /// `true` si está en pantalla completa.
35    pub fullscreen: bool,
36}
37
38impl Surface {
39    fn new(app_id: String, title: String) -> Self {
40        Self {
41            app_id,
42            title,
43            geometry: None,
44            visible: false,
45            focused: false,
46            floating: false,
47            fullscreen: false,
48        }
49    }
50}
51
52/// Una orden concreta para el backend (smithay, headless, …).
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum BodyOp {
55    /// Recoloca una superficie, la muestra u oculta y dice si flota o
56    /// está en pantalla completa (el backend ajusta el orden de pintado
57    /// y el estado `xdg_toplevel` en consecuencia).
58    Configure {
59        id: WindowId,
60        rect: Rect,
61        visible: bool,
62        floating: bool,
63        fullscreen: bool,
64    },
65    /// Da el foco del teclado a una superficie.
66    Focus(WindowId),
67    /// Quita el foco a todas las superficies.
68    Unfocus,
69    /// Pide el cierre ordenado de un cliente.
70    CloseClient(WindowId),
71    /// Mata a un cliente que no responde.
72    KillClient(WindowId),
73    /// Registra los atajos globales a interceptar.
74    SetGrabs(Vec<String>),
75    /// Cambia el cursor del puntero.
76    SetCursor(String),
77    /// Lanza un programa como proceso hijo del compositor.
78    Spawn(String),
79    /// Apaga el compositor y libera el hardware.
80    Shutdown,
81}
82
83/// La contabilidad del Cuerpo: salidas y superficies.
84#[derive(Debug, Default)]
85pub struct BodyState {
86    outputs: Vec<(OutputId, Rect)>,
87    /// `BTreeMap` para que el orden de las `BodyOp` sea determinista.
88    surfaces: BTreeMap<WindowId, Surface>,
89    focused: Option<WindowId>,
90}
91
92impl BodyState {
93    /// Cuerpo recién arrancado: sin salidas ni superficies.
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    // --- Traducción de comandos del Cerebro --------------------------
99
100    /// Traduce un comando del Cerebro a las operaciones de backend que lo
101    /// materializan. Sólo emite lo que de verdad cambia: un `Place`
102    /// idéntico al estado actual no produce ninguna `BodyOp`.
103    pub fn apply(&mut self, cmd: BrainCommand) -> Vec<BodyOp> {
104        match cmd {
105            BrainCommand::Place(placements) => {
106                let mut ops = Vec::new();
107                let listed: Vec<WindowId> = placements.iter().map(|p| p.id).collect();
108                let mut new_focus = None;
109
110                // Reconfigura las superficies que aparecen en la lista.
111                for p in &placements {
112                    if p.focused {
113                        new_focus = Some(p.id);
114                    }
115                    if let Some(s) = self.surfaces.get_mut(&p.id) {
116                        if s.geometry != Some(p.rect)
117                            || s.visible != p.visible
118                            || s.floating != p.floating
119                            || s.fullscreen != p.fullscreen
120                        {
121                            s.geometry = Some(p.rect);
122                            s.visible = p.visible;
123                            s.floating = p.floating;
124                            s.fullscreen = p.fullscreen;
125                            ops.push(BodyOp::Configure {
126                                id: p.id,
127                                rect: p.rect,
128                                visible: p.visible,
129                                floating: p.floating,
130                                fullscreen: p.fullscreen,
131                            });
132                        }
133                    }
134                }
135
136                // Oculta lo que el Cerebro ya no coloca.
137                for (id, s) in &mut self.surfaces {
138                    if !listed.contains(id) && s.visible {
139                        s.visible = false;
140                        let rect = s.geometry.unwrap_or(Rect::new(0, 0, 0, 0));
141                        ops.push(BodyOp::Configure {
142                            id: *id,
143                            rect,
144                            visible: false,
145                            floating: s.floating,
146                            fullscreen: s.fullscreen,
147                        });
148                    }
149                }
150
151                // Reasigna el foco sólo si cambió.
152                if new_focus != self.focused {
153                    self.focused = new_focus;
154                    for (id, s) in &mut self.surfaces {
155                        s.focused = Some(*id) == new_focus;
156                    }
157                    ops.push(match new_focus {
158                        Some(id) => BodyOp::Focus(id),
159                        None => BodyOp::Unfocus,
160                    });
161                }
162                ops
163            }
164            BrainCommand::Close(id) => vec![BodyOp::CloseClient(id)],
165            BrainCommand::Kill(id) => vec![BodyOp::KillClient(id)],
166            BrainCommand::GrabKeys(keys) => vec![BodyOp::SetGrabs(keys)],
167            BrainCommand::SetCursor(name) => vec![BodyOp::SetCursor(name)],
168            BrainCommand::Spawn(cmd) => vec![BodyOp::Spawn(cmd)],
169            BrainCommand::Shutdown => vec![BodyOp::Shutdown],
170        }
171    }
172
173    // --- Mutadores del backend → eventos para el Cerebro -------------
174
175    /// Registra una salida recién conectada.
176    pub fn add_output(&mut self, id: OutputId, width: i32, height: i32) -> BodyEvent {
177        self.outputs.push((id, Rect::new(0, 0, width, height)));
178        BodyEvent::OutputAdded { id, width, height }
179    }
180
181    /// Da de baja una salida desconectada.
182    pub fn remove_output(&mut self, id: OutputId) -> BodyEvent {
183        self.outputs.retain(|(o, _)| *o != id);
184        BodyEvent::OutputRemoved { id }
185    }
186
187    /// Cambia el área útil de una salida sin desconectarla — al
188    /// redimensionar la ventana anfitriona o al reservar/liberar la
189    /// franja del shell. Conserva el escritorio que muestra.
190    pub fn resize_output(&mut self, id: OutputId, width: i32, height: i32) -> BodyEvent {
191        if let Some((_, rect)) = self.outputs.iter_mut().find(|(o, _)| *o == id) {
192            rect.w = width;
193            rect.h = height;
194        }
195        BodyEvent::OutputResized { id, width, height }
196    }
197
198    /// Registra una superficie recién creada por un cliente.
199    pub fn open_surface(
200        &mut self,
201        id: WindowId,
202        app_id: impl Into<String>,
203        title: impl Into<String>,
204    ) -> BodyEvent {
205        let app_id = app_id.into();
206        let title = title.into();
207        self.surfaces
208            .insert(id, Surface::new(app_id.clone(), title.clone()));
209        BodyEvent::WindowOpened { id, app_id, title }
210    }
211
212    /// Da de baja una superficie destruida. `None` si no se conocía.
213    pub fn close_surface(&mut self, id: WindowId) -> Option<BodyEvent> {
214        self.surfaces.remove(&id)?;
215        if self.focused == Some(id) {
216            self.focused = None;
217        }
218        Some(BodyEvent::WindowClosed { id })
219    }
220
221    /// Actualiza el título de una superficie. `None` si no se conocía.
222    pub fn retitle_surface(&mut self, id: WindowId, title: impl Into<String>) -> Option<BodyEvent> {
223        let title = title.into();
224        let s = self.surfaces.get_mut(&id)?;
225        s.title = title.clone();
226        Some(BodyEvent::WindowRetitled { id, title })
227    }
228
229    /// Construye un evento de puntero entrando en una superficie.
230    pub fn pointer_enter(&self, id: WindowId) -> BodyEvent {
231        BodyEvent::PointerEntered { id }
232    }
233
234    /// Construye un evento de atajo pulsado.
235    pub fn keybind(&self, combo: impl Into<String>) -> BodyEvent {
236        BodyEvent::Keybind(combo.into())
237    }
238
239    // --- Accesores de sólo lectura -----------------------------------
240
241    /// Las salidas conectadas.
242    pub fn outputs(&self) -> &[(OutputId, Rect)] {
243        &self.outputs
244    }
245
246    /// Una superficie conocida.
247    pub fn surface(&self, id: WindowId) -> Option<&Surface> {
248        self.surfaces.get(&id)
249    }
250
251    /// Número de superficies registradas.
252    pub fn surface_count(&self) -> usize {
253        self.surfaces.len()
254    }
255
256    /// Las superficies visibles, en orden de id.
257    pub fn visible(&self) -> impl Iterator<Item = (WindowId, &Surface)> {
258        self.surfaces.iter().filter(|(_, s)| s.visible).map(|(id, s)| (*id, s))
259    }
260
261    /// La superficie enfocada.
262    pub fn focused(&self) -> Option<WindowId> {
263        self.focused
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use mirada_protocol::WindowPlacement;
271
272    fn placement(id: WindowId, visible: bool, focused: bool) -> WindowPlacement {
273        WindowPlacement {
274            id,
275            rect: Rect::new(0, 0, 800, 600),
276            visible,
277            focused,
278            floating: false,
279            fullscreen: false,
280        }
281    }
282
283    /// Cuerpo con dos superficies abiertas.
284    fn body_with_two() -> BodyState {
285        let mut b = BodyState::new();
286        b.add_output(0, 1920, 1080);
287        b.open_surface(1, "app1", "uno");
288        b.open_surface(2, "app2", "dos");
289        b
290    }
291
292    #[test]
293    fn opening_a_surface_yields_a_window_opened_event() {
294        let mut b = BodyState::new();
295        let ev = b.open_surface(7, "org.brahman.shuma", "shell");
296        assert_eq!(
297            ev,
298            BodyEvent::WindowOpened {
299                id: 7,
300                app_id: "org.brahman.shuma".into(),
301                title: "shell".into()
302            }
303        );
304        assert_eq!(b.surface_count(), 1);
305    }
306
307    #[test]
308    fn placing_surfaces_configures_and_focuses_them() {
309        let mut b = body_with_two();
310        let ops = b.apply(BrainCommand::Place(vec![
311            placement(1, true, false),
312            placement(2, true, true),
313        ]));
314        // Dos Configure + un Focus.
315        let configures = ops.iter().filter(|o| matches!(o, BodyOp::Configure { .. })).count();
316        assert_eq!(configures, 2);
317        assert!(ops.contains(&BodyOp::Focus(2)));
318        assert_eq!(b.focused(), Some(2));
319        assert!(b.surface(2).unwrap().focused);
320    }
321
322    #[test]
323    fn an_identical_place_produces_no_ops() {
324        let mut b = body_with_two();
325        let cmd = BrainCommand::Place(vec![placement(1, true, true), placement(2, true, false)]);
326        assert!(!b.apply(cmd.clone()).is_empty());
327        // Repetir el mismo Place no cambia nada.
328        assert!(b.apply(cmd).is_empty());
329    }
330
331    #[test]
332    fn dropping_a_surface_from_the_list_hides_it() {
333        let mut b = body_with_two();
334        b.apply(BrainCommand::Place(vec![placement(1, true, true), placement(2, true, false)]));
335        // El Cerebro deja de colocar la 2 (p. ej. cambió de escritorio).
336        let ops = b.apply(BrainCommand::Place(vec![placement(1, true, true)]));
337        assert!(ops.contains(&BodyOp::Configure {
338            id: 2,
339            rect: Rect::new(0, 0, 800, 600),
340            visible: false,
341            floating: false,
342            fullscreen: false,
343        }));
344        assert!(!b.surface(2).unwrap().visible);
345    }
346
347    #[test]
348    fn placement_for_an_unknown_surface_is_ignored() {
349        let mut b = body_with_two();
350        // La 99 no existe — no debe producir Configure.
351        let ops = b.apply(BrainCommand::Place(vec![placement(99, true, true)]));
352        assert!(!ops.iter().any(|o| matches!(o, BodyOp::Configure { id: 99, .. })));
353    }
354
355    #[test]
356    fn close_and_kill_map_to_client_ops() {
357        let mut b = body_with_two();
358        assert_eq!(b.apply(BrainCommand::Close(1)), vec![BodyOp::CloseClient(1)]);
359        assert_eq!(b.apply(BrainCommand::Kill(2)), vec![BodyOp::KillClient(2)]);
360    }
361
362    #[test]
363    fn grab_keys_cursor_and_shutdown_pass_through() {
364        let mut b = BodyState::new();
365        assert_eq!(
366            b.apply(BrainCommand::GrabKeys(vec!["Super+q".into()])),
367            vec![BodyOp::SetGrabs(vec!["Super+q".into()])]
368        );
369        assert_eq!(
370            b.apply(BrainCommand::SetCursor("crosshair".into())),
371            vec![BodyOp::SetCursor("crosshair".into())]
372        );
373        assert_eq!(b.apply(BrainCommand::Shutdown), vec![BodyOp::Shutdown]);
374    }
375
376    #[test]
377    fn closing_a_surface_clears_its_focus() {
378        let mut b = body_with_two();
379        b.apply(BrainCommand::Place(vec![placement(1, true, true)]));
380        assert_eq!(b.focused(), Some(1));
381        let ev = b.close_surface(1);
382        assert_eq!(ev, Some(BodyEvent::WindowClosed { id: 1 }));
383        assert_eq!(b.focused(), None);
384        assert_eq!(b.surface_count(), 1);
385    }
386
387    #[test]
388    fn closing_an_unknown_surface_yields_nothing() {
389        let mut b = body_with_two();
390        assert!(b.close_surface(404).is_none());
391    }
392
393    #[test]
394    fn retitling_updates_the_surface() {
395        let mut b = body_with_two();
396        let ev = b.retitle_surface(1, "uno bis");
397        assert_eq!(ev, Some(BodyEvent::WindowRetitled { id: 1, title: "uno bis".into() }));
398        assert_eq!(b.surface(1).unwrap().title, "uno bis");
399    }
400
401    #[test]
402    fn outputs_are_tracked() {
403        let mut b = BodyState::new();
404        b.add_output(0, 2560, 1440);
405        b.add_output(1, 1920, 1080);
406        assert_eq!(b.outputs().len(), 2);
407        b.remove_output(0);
408        assert_eq!(b.outputs().len(), 1);
409        assert_eq!(b.outputs()[0].0, 1);
410    }
411
412    #[test]
413    fn moving_focus_emits_a_single_focus_op() {
414        let mut b = body_with_two();
415        b.apply(BrainCommand::Place(vec![placement(1, true, true), placement(2, true, false)]));
416        // Cambia el foco a la 2; geometría igual → sólo un Focus.
417        let ops = b.apply(BrainCommand::Place(vec![
418            placement(1, true, false),
419            placement(2, true, true),
420        ]));
421        assert_eq!(ops, vec![BodyOp::Focus(2)]);
422    }
423
424    #[test]
425    fn a_floating_change_alone_triggers_a_configure() {
426        let mut b = body_with_two();
427        let mut p1 = placement(1, true, true);
428        b.apply(BrainCommand::Place(vec![p1, placement(2, true, false)]));
429        // Sólo cambia `floating` — misma geometría y visibilidad.
430        p1.floating = true;
431        let ops = b.apply(BrainCommand::Place(vec![p1, placement(2, true, false)]));
432        assert!(ops
433            .iter()
434            .any(|o| matches!(o, BodyOp::Configure { id: 1, floating: true, .. })));
435        assert!(b.surface(1).unwrap().floating);
436    }
437
438    #[test]
439    fn visible_iterates_only_shown_surfaces() {
440        let mut b = body_with_two();
441        b.apply(BrainCommand::Place(vec![placement(1, true, true), placement(2, false, false)]));
442        let shown: Vec<_> = b.visible().map(|(id, _)| id).collect();
443        assert_eq!(shown, vec![1]);
444    }
445}