feat(mirada): fullscreen iniciado por el cliente + HUD multi-salida
Dos remates de la tanda WM.
Fullscreen del cliente:
- BodyEvent::FullscreenRequest { id, fullscreen }. mirada-compositor
implementa XdgShellHandler::fullscreen_request / unfullscreen_request
y avisa al Cerebro; Desktop::on_event fija el fullscreen en el
escritorio que tiene la ventana. Así un reproductor o un juego que
llama a xdg set_fullscreen entra a pantalla completa solo.
HUD multi-salida (app mirada):
- El lienzo dibuja todas las salidas a escala (encaja su caja
envolvente en el lienzo fijo; con una salida, 1:1), cada una con su
marco y su número/escritorio. En simulación, Shift+n añade un monitor.
mirada-brain 63->65 tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ use smithay::input::{Seat, SeatHandler, SeatState};
|
|||||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||||
use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
|
use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
|
||||||
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_output;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_seat;
|
use smithay::reexports::wayland_server::protocol::wl_seat;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
use smithay::reexports::wayland_server::{Client, Display, ListeningSocket};
|
use smithay::reexports::wayland_server::{Client, Display, ListeningSocket};
|
||||||
@@ -335,6 +336,32 @@ impl XdgShellHandler for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fullscreen_request(
|
||||||
|
&mut self,
|
||||||
|
surface: ToplevelSurface,
|
||||||
|
_output: Option<wl_output::WlOutput>,
|
||||||
|
) {
|
||||||
|
let id = self
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.find(|w| w.surface == *surface.wl_surface())
|
||||||
|
.map(|w| w.id);
|
||||||
|
if let Some(id) = id {
|
||||||
|
self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
|
||||||
|
let id = self
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.find(|w| w.surface == *surface.wl_surface())
|
||||||
|
.map(|w| w.id);
|
||||||
|
if let Some(id) = id {
|
||||||
|
self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
|
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
|
||||||
let _ = surface.send_configure();
|
let _ = surface.send_configure();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
//! Teclas (simulación):
|
//! Teclas (simulación):
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! n abre una ventana tab / espacio cicla layout
|
//! n / Shift+n abre ventana / monitor tab / espacio cicla layout
|
||||||
//! w cierra la enfocada t m g c r d s layout directo
|
//! w cierra la enfocada t m g c r d s layout directo
|
||||||
//! f / Shift+f flota / pantalla completa h / l área maestra −/+
|
//! f / Shift+f flota / pantalla completa h / l área maestra −/+
|
||||||
//! j / k foco siguiente/anterior , / . nmaster −/+
|
//! j / k foco siguiente/anterior , / . nmaster −/+
|
||||||
@@ -282,6 +282,11 @@ impl Mirada {
|
|||||||
let connected = self.link.is_some();
|
let connected = self.link.is_some();
|
||||||
|
|
||||||
match ks.key.as_str() {
|
match ks.key.as_str() {
|
||||||
|
"n" if shift && !connected => {
|
||||||
|
// Simulación: añade un monitor más, en fila a la derecha.
|
||||||
|
let id = self.desktop.outputs().len() as u32;
|
||||||
|
self.feed(BodyEvent::OutputAdded { id, width: SCREEN_W, height: SCREEN_H });
|
||||||
|
}
|
||||||
"n" if !connected => self.open_window(),
|
"n" if !connected => self.open_window(),
|
||||||
"w" => self.act(DesktopAction::CloseFocused),
|
"w" => self.act(DesktopAction::CloseFocused),
|
||||||
"f" if shift => self.act(DesktopAction::ToggleFullscreen),
|
"f" if shift => self.act(DesktopAction::ToggleFullscreen),
|
||||||
@@ -428,7 +433,22 @@ impl Render for Mirada {
|
|||||||
.child(SharedString::from(format!("foco: {focus_label}"))),
|
.child(SharedString::from(format!("foco: {focus_label}"))),
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Lienzo: el escritorio teselado --------------------------
|
// --- Lienzo: el escritorio teselado, a escala ----------------
|
||||||
|
// El lienzo es de tamaño fijo; el contenido vive en el espacio
|
||||||
|
// global de las salidas. `scale` encaja ese espacio en el lienzo
|
||||||
|
// — con una sola salida, escala 1:1.
|
||||||
|
let outs = self.desktop.outputs();
|
||||||
|
let (bb_w, bb_h) = if outs.is_empty() {
|
||||||
|
(SCREEN_W as f32, SCREEN_H as f32)
|
||||||
|
} else {
|
||||||
|
let w = outs.iter().map(|o| o.rect.x + o.rect.w).max().unwrap_or(SCREEN_W);
|
||||||
|
let h = outs.iter().map(|o| o.rect.y + o.rect.h).max().unwrap_or(SCREEN_H);
|
||||||
|
(w as f32, h as f32)
|
||||||
|
};
|
||||||
|
let scale = (SCREEN_W as f32 / bb_w)
|
||||||
|
.min(SCREEN_H as f32 / bb_h)
|
||||||
|
.min(1.0);
|
||||||
|
|
||||||
let mut canvas = div()
|
let mut canvas = div()
|
||||||
.relative()
|
.relative()
|
||||||
.w(px(SCREEN_W as f32))
|
.w(px(SCREEN_W as f32))
|
||||||
@@ -436,6 +456,37 @@ impl Render for Mirada {
|
|||||||
.bg(canvas_bg)
|
.bg(canvas_bg)
|
||||||
.overflow_hidden();
|
.overflow_hidden();
|
||||||
|
|
||||||
|
// Un marco por salida, con su número y el escritorio que muestra.
|
||||||
|
for (i, o) in outs.iter().enumerate() {
|
||||||
|
let is_focused_out = i == self.desktop.focused_output();
|
||||||
|
canvas = canvas.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(px(o.rect.x as f32 * scale))
|
||||||
|
.top(px(o.rect.y as f32 * scale))
|
||||||
|
.w(px(o.rect.w as f32 * scale))
|
||||||
|
.h(px(o.rect.h as f32 * scale))
|
||||||
|
.border_1()
|
||||||
|
.border_color(if is_focused_out {
|
||||||
|
theme.accent
|
||||||
|
} else {
|
||||||
|
theme.border
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.top(px(2.))
|
||||||
|
.left(px(4.))
|
||||||
|
.text_color(theme.fg_disabled)
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"salida {} · escritorio {}",
|
||||||
|
i + 1,
|
||||||
|
o.workspace + 1
|
||||||
|
))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let visible = self.placements.iter().filter(|p| p.visible).count();
|
let visible = self.placements.iter().filter(|p| p.visible).count();
|
||||||
if visible == 0 {
|
if visible == 0 {
|
||||||
canvas = canvas.child(
|
canvas = canvas.child(
|
||||||
@@ -470,10 +521,10 @@ impl Render for Mirada {
|
|||||||
canvas = canvas.child(
|
canvas = canvas.child(
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
.left(px(p.rect.x as f32))
|
.left(px(p.rect.x as f32 * scale))
|
||||||
.top(px(p.rect.y as f32))
|
.top(px(p.rect.y as f32 * scale))
|
||||||
.w(px(p.rect.w as f32))
|
.w(px(p.rect.w as f32 * scale))
|
||||||
.h(px(p.rect.h as f32))
|
.h(px(p.rect.h as f32 * scale))
|
||||||
.border_2()
|
.border_2()
|
||||||
.border_color(border)
|
.border_color(border)
|
||||||
.bg(win_bg)
|
.bg(win_bg)
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ ejecuta operaciones de geometría".
|
|||||||
devuelven los `BodyEvent` a mandar. Ejemplo `headless`: un Cuerpo sin
|
devuelven los `BodyEvent` a mandar. Ejemplo `headless`: un Cuerpo sin
|
||||||
gráficos guiado por stdin para ejercitar el bucle entero.
|
gráficos guiado por stdin para ejercitar el bucle entero.
|
||||||
- **`mirada` (app)** — envuelve `Desktop` y lo pinta (barra de
|
- **`mirada` (app)** — envuelve `Desktop` y lo pinta (barra de
|
||||||
escritorios + modo + foco, lienzo teselado). Con `MIRADA_SOCKET`
|
escritorios + modo + foco, lienzo teselado). El lienzo dibuja **todas
|
||||||
|
las salidas a escala**, cada una con su marco. Con `MIRADA_SOCKET`
|
||||||
conecta a un Cuerpo; sin él corre en **simulación** (ventanas
|
conecta a un Cuerpo; sin él corre en **simulación** (ventanas
|
||||||
sintéticas, teclado de la propia ventana). Pips de escritorio y
|
sintéticas, teclado de la propia ventana). Pips de escritorio y
|
||||||
ventanas clicables.
|
ventanas clicables.
|
||||||
@@ -122,7 +123,9 @@ que un front-end (`Keybind` → `lookup` → `apply`); hay otros tres:
|
|||||||
- **Pantalla completa** — `ToggleFullscreen` (`Super+Shift+f`): la ventana
|
- **Pantalla completa** — `ToggleFullscreen` (`Super+Shift+f`): la ventana
|
||||||
cubre toda la salida (sin gap), oculta al resto y se lleva el foco;
|
cubre toda la salida (sin gap), oculta al resto y se lleva el foco;
|
||||||
`Workspace.fullscreen: Option<WindowId>`, y el Cuerpo le fija el estado
|
`Workspace.fullscreen: Option<WindowId>`, y el Cuerpo le fija el estado
|
||||||
`xdg_toplevel Fullscreen`.
|
`xdg_toplevel Fullscreen`. También atiende la petición del propio
|
||||||
|
cliente (`xdg set_fullscreen` → `BodyEvent::FullscreenRequest`), así
|
||||||
|
que un reproductor o un juego entran a pantalla completa solos.
|
||||||
- **Multi-monitor** — cada `Output` muestra un escritorio distinto;
|
- **Multi-monitor** — cada `Output` muestra un escritorio distinto;
|
||||||
`SwitchWorkspace` actúa sobre la salida enfocada (y la intercambia si
|
`SwitchWorkspace` actúa sobre la salida enfocada (y la intercambia si
|
||||||
el escritorio pedido ya lo muestra otra salida); `FocusOutputNext`
|
el escritorio pedido ya lo muestra otra salida); `FocusOutputNext`
|
||||||
@@ -175,7 +178,7 @@ a las ya abiertas.
|
|||||||
## Estado
|
## Estado
|
||||||
|
|
||||||
Implementado y verde: `mirada-layout` (32 tests), `mirada-protocol`
|
Implementado y verde: `mirada-layout` (32 tests), `mirada-protocol`
|
||||||
(11), `mirada-brain` (63), `mirada-link` (7), `mirada-body` (14), las
|
(11), `mirada-brain` (65), `mirada-link` (7), `mirada-body` (14), las
|
||||||
apps `mirada` y `mirada-compositor` (compilan; verificación visual
|
apps `mirada` y `mirada-compositor` (compilan; verificación visual
|
||||||
manual) y `mirada-ctl` (CLI, probado vía el ejemplo `headless-ctl`).
|
manual) y `mirada-ctl` (CLI, probado vía el ejemplo `headless-ctl`).
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,27 @@ impl Desktop {
|
|||||||
Some(action) => self.apply(action),
|
Some(action) => self.apply(action),
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
},
|
},
|
||||||
|
BodyEvent::FullscreenRequest { id, fullscreen } => {
|
||||||
|
// El cliente (un reproductor, un juego) pidió pantalla
|
||||||
|
// completa: la fijamos en el escritorio que tiene la ventana.
|
||||||
|
let mut changed = false;
|
||||||
|
for ws in &mut self.workspaces {
|
||||||
|
if ws.windows().contains(&id) {
|
||||||
|
if fullscreen {
|
||||||
|
ws.set_fullscreen(Some(id));
|
||||||
|
} else if ws.fullscreen() == Some(id) {
|
||||||
|
ws.set_fullscreen(None);
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
self.relayout()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -985,6 +1006,27 @@ mod tests {
|
|||||||
assert!(d.apply(DesktopAction::ToggleScratchpad).is_empty());
|
assert!(d.apply(DesktopAction::ToggleScratchpad).is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_client_fullscreen_request_is_honoured() {
|
||||||
|
let mut d = desktop_with_screen();
|
||||||
|
open(&mut d, 1);
|
||||||
|
open(&mut d, 2);
|
||||||
|
let cmds = d.on_event(BodyEvent::FullscreenRequest { id: 1, fullscreen: true });
|
||||||
|
assert!(places(&cmds).iter().find(|x| x.id == 1).unwrap().fullscreen);
|
||||||
|
// El cliente la suelta.
|
||||||
|
let cmds = d.on_event(BodyEvent::FullscreenRequest { id: 1, fullscreen: false });
|
||||||
|
assert!(!places(&cmds).iter().find(|x| x.id == 1).unwrap().fullscreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fullscreen_request_for_an_unknown_window_does_nothing() {
|
||||||
|
let mut d = desktop_with_screen();
|
||||||
|
open(&mut d, 1);
|
||||||
|
assert!(d
|
||||||
|
.on_event(BodyEvent::FullscreenRequest { id: 99, fullscreen: true })
|
||||||
|
.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn window_lines_show_a_stashed_window_as_workspace_zero() {
|
fn window_lines_show_a_stashed_window_as_workspace_zero() {
|
||||||
let mut d = desktop_with_screen();
|
let mut d = desktop_with_screen();
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ pub enum BodyEvent {
|
|||||||
Keybind(String),
|
Keybind(String),
|
||||||
/// El puntero entró en una ventana — el Cerebro puede enfocar al pasar.
|
/// El puntero entró en una ventana — el Cerebro puede enfocar al pasar.
|
||||||
PointerEntered { id: WindowId },
|
PointerEntered { id: WindowId },
|
||||||
|
/// Un cliente pidió pantalla completa para su ventana (`true`), o la
|
||||||
|
/// soltó (`false`) — `xdg_toplevel.set_fullscreen`.
|
||||||
|
FullscreenRequest { id: WindowId, fullscreen: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tamaño máximo de un marco, en bytes. Acota el búfer de [`read_frame`]
|
/// Tamaño máximo de un marco, en bytes. Acota el búfer de [`read_frame`]
|
||||||
|
|||||||
@@ -1090,5 +1090,13 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Fullscreen iniciado por el cliente + HUD multi-salida:
|
||||||
|
BodyEvent::FullscreenRequest — cuando un cliente pide xdg set_fullscreen, el Cuerpo
|
||||||
|
(XdgShellHandler::fullscreen_request) avisa al Cerebro y la ventana entra a pantalla completa sola.
|
||||||
|
El lienzo de la app mirada dibuja ahora todas las salidas a escala, cada una con su marco;
|
||||||
|
en simulación, Shift+n añade un monitor más.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user