gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Definiciones

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

Fase 2 del plan «shell»: carmen reconoce la ventana del shell y le
reserva su sitio, en vez de teselarla como una más.

Una ventana cuyo `app_id` es `carmen.shell` no entra en el teselado:
carmen le reserva una franja de 40 px al pie de la salida, la dimensiona
y la fija ahí, y la compone sobre todas las demás. El Cerebro tesela el
resto de ventanas en el área que queda.

- `mirada-protocol`: nuevo `BodyEvent::OutputResized { id, w, h }` — el
  Cerebro cambia el área útil de una salida **sin** perder el escritorio
  que muestra (a diferencia de quitar y volver a añadir la salida — que,
  de paso, era un bug latente al redimensionar la ventana winit).
- `mirada-brain`: `Desktop` atiende `OutputResized` (test nuevo).
- `mirada-body`: `BodyState::resize_output`.
- `mirada-compositor`: `ManagedWindow.is_shell`, `App.output_size`,
  `dock_shell`/`output_changed`; `register_toplevel` no registra el
  shell en el Cerebro; al cerrarse libera la franja. El shell se compone
  y se enfoca con el ratón aunque no viva en el Cerebro; no lleva marco.
  El backend winit usa ahora `resize_output` al redimensionar.

GPUI no habla `wlr-layer-shell`, así que el acople es por `app_id`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

ee27108f6c9abe63ac3abdca743da82b467fc2ca · por sergio · (hace 5 meses) · padre 7b5c583a9 · ver el árbol en este commit

Delta semántico · 18

crates/apps/mirada-compositor/src/drm_backend.rs

crates/apps/mirada-compositor/src/main.rs

crates/modules/mirada/mirada-body/src/lib.rs

crates/modules/mirada/mirada-brain/src/desktop.rs

crates/modules/mirada/mirada-protocol/src/lib.rs

7 archivos · +178 −38

diff --git a/crates/apps/mirada-compositor/src/drm_backend.rs b/crates/apps/mirada-compositor/src/drm_backend.rs
index 0cac095..34f0933 100644
--- a/crates/apps/mirada-compositor/src/drm_backend.rs
+++ b/crates/apps/mirada-compositor/src/drm_backend.rs
@@ -157,8 +157,8 @@ impl DrmState {
         // tamaño (sigue al contenido) y su color (según el foco). Cada
         // `SolidColorBuffer` sube su contador de daño sólo si algo cambió.
         for w in &mut self.app.windows {
-            if !w.visible {
-                continue;
+            if !w.visible || w.is_shell {
+                continue; // el shell no lleva marco
             }
             let (x, y) = crate::render_loc(w);
             let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
@@ -210,21 +210,27 @@ impl DrmState {
                 }
             }
 
+            // El shell va sobre todo; luego las flotantes; luego las
+            // teseladas. `sort_by_key` es estable: respeta el orden de
+            // apertura dentro de cada grupo.
             let mut shown: Vec<_> = self.app.windows.iter().filter(|w| w.visible).collect();
-            shown.sort_by_key(|w| !w.floating);
+            shown.sort_by_key(|w| (!w.is_shell, !w.floating));
             for w in &shown {
                 let (x, y) = crate::render_loc(w);
                 let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
-                let rects = border_rects(x, y, sw, sh);
-                // El marco, encima de la propia superficie de la ventana.
-                for (buf, (bx, by, _, _)) in w.borders.iter().zip(rects) {
-                    out.push(Frame::Solid(SolidColorRenderElement::from_buffer(
-                        buf,
-                        (bx, by),
-                        1.0,
-                        1.0,
-                        Kind::Unspecified,
-                    )));
+                // El marco, encima de la propia superficie de la ventana
+                // — el shell no lleva.
+                if !w.is_shell {
+                    let rects = border_rects(x, y, sw, sh);
+                    for (buf, (bx, by, _, _)) in w.borders.iter().zip(rects) {
+                        out.push(Frame::Solid(SolidColorRenderElement::from_buffer(
+                            buf,
+                            (bx, by),
+                            1.0,
+                            1.0,
+                            Kind::Unspecified,
+                        )));
+                    }
                 }
                 for el in render_elements_from_surface_tree(
                     &mut self.renderer,
@@ -525,13 +531,25 @@ impl DrmState {
             self.app.cursor_status = CursorImageStatus::default_named();
         }
 
-        // Foco-sigue-ratón: al pasar a otra ventana, que el Cerebro la enfoque.
+        // Foco-sigue-ratón: al pasar a otra ventana, que la enfoque quien
+        // corresponda — el Cerebro para las teseladas, carmen mismo para
+        // el shell (que no vive en el Cerebro).
         let hovered = hit.map(|i| self.app.windows[i].id);
         if hovered != self.last_pointer_window {
             self.last_pointer_window = hovered;
-            if let Some(id) = hovered {
-                let ev = self.app.body.pointer_enter(id);
-                self.app.brain_feed(ev);
+            match hit {
+                Some(i) if self.app.windows[i].is_shell => {
+                    let surf = self.app.windows[i].surface.clone();
+                    if let Some(kb) = self.app.keyboard.clone() {
+                        kb.set_focus(&mut self.app, Some(surf), SERIAL_COUNTER.next_serial());
+                    }
+                }
+                Some(i) => {
+                    let id = self.app.windows[i].id;
+                    let ev = self.app.body.pointer_enter(id);
+                    self.app.brain_feed(ev);
+                }
+                None => {}
             }
         }
     }
@@ -564,13 +582,17 @@ impl DrmState {
         true
     }
 
-    /// El índice de la ventana visible bajo el punto `(x, y)`, si la hay —
-    /// en orden front-to-back (las flotantes ganan a las teseladas).
+    /// El índice de la ventana visible bajo el punto `(x, y)`, si la hay
+    /// — en orden front-to-back (el shell gana a las flotantes, y éstas a
+    /// las teseladas).
     fn window_at(&self, x: f64, y: f64) -> Option<usize> {
         let mut idx: Vec<usize> = (0..self.app.windows.len())
             .filter(|&i| self.app.windows[i].visible)
             .collect();
-        idx.sort_by_key(|&i| !self.app.windows[i].floating);
+        idx.sort_by_key(|&i| {
+            let w = &self.app.windows[i];
+            (!w.is_shell, !w.floating)
+        });
         idx.into_iter().find(|&i| {
             let w = &self.app.windows[i];
             let (lx, ly) = crate::render_loc(w);
@@ -716,6 +738,7 @@ pub fn run() -> Result<(), Box<dyn Error>> {
     // La salida del Cerebro = el modo del monitor.
     let ev = app.body.add_output(0, mode_w as i32, mode_h as i32);
     app.brain_feed(ev);
+    app.output_size = (mode_w as i32, mode_h as i32);
     // El puntero arranca en el centro de la pantalla.
     app.pointer_loc = (mode_w as f64 / 2.0, mode_h as f64 / 2.0);
     // Anuncia el monitor en el protocolo Wayland — los clientes lo exigen.
diff --git a/crates/apps/mirada-compositor/src/main.rs b/crates/apps/mirada-compositor/src/main.rs
index 6c3804d..04032dc 100644
--- a/crates/apps/mirada-compositor/src/main.rs
+++ b/crates/apps/mirada-compositor/src/main.rs
@@ -89,6 +89,13 @@ enum Brain {
     Linked(BodyLink),
 }
 
+/// `app_id` que distingue a la ventana del shell del escritorio. carmen
+/// no la tesela: la acopla a una franja al pie de la pantalla.
+const SHELL_APP_ID: &str = "carmen.shell";
+
+/// Alto en píxeles de la franja del shell, al pie de la salida.
+const SHELL_DOCK_HEIGHT: i32 = 40;
+
 /// Una ventana de cliente que el compositor gestiona.
 struct ManagedWindow {
     id: u64,
@@ -104,6 +111,8 @@ struct ManagedWindow {
     floating: bool,
     /// `true` si tiene el foco del teclado — pinta el marco resaltado.
     focused: bool,
+    /// `true` si es la ventana del shell — acoplada al pie, sin teselar.
+    is_shell: bool,
     /// Búferes de los 4 lados del marco (arriba, abajo, izq., der.) —
     /// cada uno con su `Id` estable para el seguimiento de daño.
     borders: [SolidColorBuffer; 4],
@@ -150,6 +159,9 @@ struct App {
     cursor_status: CursorImageStatus,
     /// Arrastre de ventana en curso (mover o redimensionar con el ratón).
     drag: Option<DragGrab>,
+    /// Tamaño real de la salida (con la franja del shell incluida) — lo
+    /// fija el backend; sirve para acoplar la ventana del shell.
+    output_size: (i32, i32),
 
     /// Ventanas gestionadas, en orden de aparición.
     windows: Vec<ManagedWindow>,
@@ -304,8 +316,8 @@ impl App {
                 })
                 .unwrap_or_default()
         });
-        let app_id = if app_id.is_empty() { "cliente".into() } else { app_id };
-        let title = if title.is_empty() { format!("ventana {id}") } else { title };
+        // La ventana del shell no se tesela: carmen la acopla al pie.
+        let is_shell = app_id == SHELL_APP_ID;
 
         self.windows.push(ManagedWindow {
             id,
@@ -316,10 +328,55 @@ impl App {
             visible: false,
             floating: false,
             focused: false,
+            is_shell,
             borders: std::array::from_fn(|_| SolidColorBuffer::default()),
         });
-        let ev = self.body.open_surface(id, app_id, title);
+
+        if is_shell {
+            self.dock_shell();
+        } else {
+            let app_id = if app_id.is_empty() { "cliente".into() } else { app_id };
+            let title = if title.is_empty() { format!("ventana {id}") } else { title };
+            let ev = self.body.open_surface(id, app_id, title);
+            self.brain_feed(ev);
+        }
+    }
+
+    /// Acopla la ventana del shell: le reserva una franja al pie de la
+    /// salida —el Cerebro tesela el área que queda— y la dimensiona y
+    /// coloca ahí. Se llama al registrarla y al cambiar el tamaño de la
+    /// salida.
+    fn dock_shell(&mut self) {
+        let (ow, oh) = self.output_size;
+        if ow == 0 || oh == 0 {
+            return; // la salida todavía no está lista
+        }
+        // Reserva la franja: el Cerebro tesela en el alto que queda.
+        let ev = self.body.resize_output(0, ow, oh - SHELL_DOCK_HEIGHT);
         self.brain_feed(ev);
+        // Dimensiona la ventana del shell y la fija en la franja.
+        if let Some(w) = self.windows.iter_mut().find(|w| w.is_shell) {
+            w.loc = (0, oh - SHELL_DOCK_HEIGHT);
+            w.size = (ow, SHELL_DOCK_HEIGHT);
+            w.visible = true;
+            w.toplevel.with_pending_state(|s| {
+                s.size = Some((ow.max(1), SHELL_DOCK_HEIGHT.max(1)).into());
+            });
+            w.toplevel.send_pending_configure();
+        }
+    }
+
+    /// El backend informa de un tamaño de salida nuevo (arranque o
+    /// redimensión). Si hay shell acoplado, recoloca su franja; si no,
+    /// le pasa el área entera al Cerebro.
+    fn output_changed(&mut self, width: i32, height: i32) {
+        self.output_size = (width, height);
+        if self.windows.iter().any(|w| w.is_shell) {
+            self.dock_shell();
+        } else {
+            let ev = self.body.resize_output(0, width, height);
+            self.brain_feed(ev);
+        }
     }
 }
 
@@ -388,8 +445,16 @@ impl XdgShellHandler for App {
             .iter()
             .position(|w| w.surface == *surface.wl_surface());
         if let Some(pos) = pos {
-            let id = self.windows.remove(pos).id;
-            if let Some(ev) = self.body.close_surface(id) {
+            let w = self.windows.remove(pos);
+            if w.is_shell {
+                // El shell se cerró: libera su franja, el Cerebro vuelve
+                // a teselar en la salida entera.
+                let (ow, oh) = self.output_size;
+                if ow != 0 && oh != 0 {
+                    let ev = self.body.resize_output(0, ow, oh);
+                    self.brain_feed(ev);
+                }
+            } else if let Some(ev) = self.body.close_surface(w.id) {
                 self.brain_feed(ev);
             }
         }
@@ -813,6 +878,7 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
         pointer_loc: (0.0, 0.0),
         cursor_status: CursorImageStatus::default_named(),
         drag: None,
+        output_size: (0, 0),
         windows: Vec::new(),
         body: BodyState::new(),
         brain,
@@ -925,6 +991,7 @@ fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
     {
         let ev = state.body.add_output(0, win_size.w, win_size.h);
         state.brain_feed(ev);
+        state.output_size = (win_size.w, win_size.h);
     }
 
     while state.running {
@@ -932,10 +999,7 @@ fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
         let status = winit.dispatch_new_events(|event| match event {
             WinitEvent::CloseRequested => state.running = false,
             WinitEvent::Resized { size, .. } => {
-                let ev = state.body.remove_output(0);
-                state.brain_feed(ev);
-                let ev = state.body.add_output(0, size.w, size.h);
-                state.brain_feed(ev);
+                state.output_changed(size.w, size.h);
             }
             WinitEvent::Input(InputEvent::Keyboard { event }) => {
                 let code = event.key_code();
@@ -1015,12 +1079,12 @@ fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
         {
             let (renderer, mut framebuffer) = backend.bind().unwrap();
             // Orden de pintado: la lista de elementos va front-to-back
-            // (índice 0 = encima), así que las flotantes —que deben
-            // quedar sobre las teseladas— se ordenan primero. `sort_by_key`
-            // es estable: dentro de cada grupo se respeta el orden de apertura.
+            // (índice 0 = encima): el shell primero —va sobre todo—, luego
+            // las flotantes, luego las teseladas. `sort_by_key` es estable:
+            // dentro de cada grupo se respeta el orden de apertura.
             let mut shown: Vec<&ManagedWindow> =
                 state.windows.iter().filter(|w| w.visible).collect();
-            shown.sort_by_key(|w| !w.floating);
+            shown.sort_by_key(|w| (!w.is_shell, !w.floating));
             let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = shown
                 .iter()
                 .flat_map(|w| {
diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md
index 5619e8a..0b642e7 100644
--- a/crates/modules/mirada/SDD.md
+++ b/crates/modules/mirada/SDD.md
@@ -228,14 +228,21 @@ Cerebro: **autónomo** (`Desktop` embebido) o **enlazado** (`MIRADA_SOCKET`
 - **Sesión** — `~/.config/mirada/autostart` (un comando por línea) se
   lanza al arrancar el backend DRM; el script `session/mirada-session` y
   `session/carmen.desktop` integran carmen con un gestor de login.
+- **Acople del shell** — una ventana con `app_id = "carmen.shell"` no se
+  tesela: carmen le reserva una franja de 40 px al pie de la salida y la
+  pinta sobre todo. La reserva viaja como `BodyEvent::OutputResized`, que
+  encoge el área útil del Cerebro **sin** perder el escritorio que
+  muestra (a diferencia de quitar y volver a añadir la salida). Es el
+  anclaje de la futura `shuma-shell` en modo launcher.
 
 **Pendiente** — refinamientos del Cuerpo:
 
-| capa pendiente   | rol                                                          |
-| ---------------- | ------------------------------------------------------------ |
+| capa pendiente     | rol                                                        |
+| ------------------ | ---------------------------------------------------------- |
 | puntero en `winit` | ratón en el backend anidado (hoy sólo el backend DRM)      |
-| `mirada-input`   | repetición de teclas, gestos; hotplug de monitores           |
-| barra de estado  | `wlr-layer-shell` + un cliente que dibuje la barra           |
-| `mirada-sandbox` | aislamiento de clientes sobre `arje-incarnate`               |
+| `mirada-input`     | repetición de teclas, gestos; hotplug de monitores         |
+| `shuma-shell`      | modo launcher: barra + input + cajón sobre el acople shell |
+| `wlr-layer-shell`  | barras externas tipo waybar, fondos, notificaciones        |
+| `mirada-sandbox`   | aislamiento de clientes sobre `arje-incarnate`             |
 
 CRIU (congelar/restaurar ventanas) queda anotado como futuro.
diff --git a/crates/modules/mirada/mirada-body/src/lib.rs b/crates/modules/mirada/mirada-body/src/lib.rs
index 001d559..729bcfe 100644
--- a/crates/modules/mirada/mirada-body/src/lib.rs
+++ b/crates/modules/mirada/mirada-body/src/lib.rs
@@ -184,6 +184,17 @@ impl BodyState {
         BodyEvent::OutputRemoved { id }
     }
 
+    /// Cambia el área útil de una salida sin desconectarla — al
+    /// redimensionar la ventana anfitriona o al reservar/liberar la
+    /// franja del shell. Conserva el escritorio que muestra.
+    pub fn resize_output(&mut self, id: OutputId, width: i32, height: i32) -> BodyEvent {
+        if let Some((_, rect)) = self.outputs.iter_mut().find(|(o, _)| *o == id) {
+            rect.w = width;
+            rect.h = height;
+        }
+        BodyEvent::OutputResized { id, width, height }
+    }
+
     /// Registra una superficie recién creada por un cliente.
     pub fn open_surface(
         &mut self,
diff --git a/crates/modules/mirada/mirada-brain/src/desktop.rs b/crates/modules/mirada/mirada-brain/src/desktop.rs
index cdeb3ec..a6094f9 100644
--- a/crates/modules/mirada/mirada-brain/src/desktop.rs
+++ b/crates/modules/mirada/mirada-brain/src/desktop.rs
@@ -141,6 +141,18 @@ impl Desktop {
                 self.reflow_outputs();
                 self.relayout()
             }
+            BodyEvent::OutputResized { id, width, height } => {
+                // Sólo cambia el área útil; el escritorio que muestra la
+                // salida se conserva.
+                if let Some(o) = self.outputs.iter_mut().find(|o| o.id == id) {
+                    o.rect.w = width;
+                    o.rect.h = height;
+                    self.reflow_outputs();
+                    self.relayout()
+                } else {
+                    Vec::new()
+                }
+            }
             BodyEvent::WindowOpened { id, app_id, title } => {
                 // Las reglas pueden mandarla a otro escritorio o hacerla flotar.
                 let outcome = self.rules.resolve(&app_id, &title);
@@ -877,6 +889,23 @@ mod tests {
         assert_eq!(p.rect, target);
     }
 
+    #[test]
+    fn resizing_an_output_retiles_without_losing_the_workspace() {
+        let mut d = desktop_with_screen();
+        open(&mut d, 1);
+        d.on_event(BodyEvent::Keybind("Super+2".into())); // escritorio activo → 2
+        assert_eq!(d.active_index(), 1);
+        let cmds = d.on_event(BodyEvent::OutputResized {
+            id: 0,
+            width: 1920,
+            height: 1040,
+        });
+        // A diferencia de quitar y volver a añadir la salida, el
+        // escritorio activo se conserva.
+        assert_eq!(d.active_index(), 1);
+        assert!(matches!(cmds.as_slice(), [BrainCommand::Place(_)]));
+    }
+
     #[test]
     fn a_spawn_keybind_becomes_a_spawn_command() {
         let mut d = desktop_with_screen();
diff --git a/crates/modules/mirada/mirada-protocol/src/lib.rs b/crates/modules/mirada/mirada-protocol/src/lib.rs
index aa87cde..d7257a0 100644
--- a/crates/modules/mirada/mirada-protocol/src/lib.rs
+++ b/crates/modules/mirada/mirada-protocol/src/lib.rs
@@ -82,6 +82,11 @@ pub enum BodyEvent {
     OutputAdded { id: OutputId, width: i32, height: i32 },
     /// Desapareció un monitor.
     OutputRemoved { id: OutputId },
+    /// Cambió el área útil de un monitor — porque se redimensionó la
+    /// ventana anfitriona, o porque el shell reservó o liberó su franja.
+    /// El escritorio que muestra **no** cambia (a diferencia de quitar y
+    /// volver a añadir la salida).
+    OutputResized { id: OutputId, width: i32, height: i32 },
     /// Un cliente creó una ventana de nivel superior.
     WindowOpened { id: WindowId, app_id: String, title: String },
     /// Una ventana se cerró (por el cliente o tras un [`BrainCommand::Close`]).
diff --git a/vamos.txt b/vamos.txt
index 7b42fd7..50125c1 100644
--- a/vamos.txt
+++ b/vamos.txt
@@ -1009,6 +1009,7 @@
   Lanzar programas: acción spawn:<comando> del keymap (Super+Shift+Return → spawn:foot por defecto).
   Lanzador de apps: mirada-launcher (escanea los .desktop, lista filtrable de terminal); atado a Super+p.
   Conmutación de VT: Ctrl+Alt+Fn salta a otra TTY y vuelve sin romper la sesión (pausa DRM + libinput).
+  Acople del shell: una ventana con app_id "carmen.shell" se ancla en una franja al pie; el resto tesela arriba.
   Sesión: ~/.config/mirada/autostart (un comando por línea) + script session/mirada-session + carmen.desktop.
   Ver crates/apps/mirada-compositor/README.md.