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>
This commit is contained in:
sergio
2026-05-21 05:38:12 +00:00
parent 7b5c583a98
commit ee27108f6c
7 changed files with 178 additions and 38 deletions
@@ -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();