feat(mirada): pantalla completa real — toggle-fullscreen
ToggleFullscreen (Super+Shift+f) lleva la ventana enfocada a pantalla completa: cubre toda la salida sin gap, oculta al resto y se lleva el foco. Distinto del modo Monocle (un modo de teselado): es un estado por ventana que ignora el layout. - Workspace.fullscreen: Option<WindowId>; set_fullscreen / fullscreen(); remove() lo limpia si se cierra esa ventana. - placements() da a la fullscreen el rect completo y marca al resto visible: false. WindowPlacement y BodyOp::Configure llevan fullscreen: bool. - mirada-compositor fija el estado xdg_toplevel::Fullscreen en la superficie, para que el cliente lo sepa. - Cableado en keymap, HUD de mirada y mirada-ctl. Verificado end-to-end con headless-ctl. mirada-protocol 10->11, mirada-brain 51->52. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -174,13 +174,18 @@ impl App {
|
||||
/// Ejecuta una operación concreta sobre las superficies reales.
|
||||
fn exec_op(&mut self, op: BodyOp) {
|
||||
match op {
|
||||
BodyOp::Configure { id, rect, visible, floating } => {
|
||||
BodyOp::Configure { id, rect, visible, floating, fullscreen } => {
|
||||
if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
|
||||
w.loc = (rect.x, rect.y);
|
||||
w.visible = visible;
|
||||
w.floating = floating;
|
||||
w.toplevel.with_pending_state(|s| {
|
||||
s.size = Some((rect.w.max(1), rect.h.max(1)).into());
|
||||
if fullscreen {
|
||||
s.states.set(xdg_toplevel::State::Fullscreen);
|
||||
} else {
|
||||
s.states.unset(xdg_toplevel::State::Fullscreen);
|
||||
}
|
||||
});
|
||||
w.toplevel.send_pending_configure();
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ Acciones de mirada-ctl:
|
||||
move-backward la atrasa
|
||||
close-focused cierra la ventana enfocada
|
||||
toggle-float alterna flotante / teselada la enfocada
|
||||
toggle-fullscreen alterna pantalla completa en la enfocada
|
||||
cycle-layout pasa al siguiente modo de teselado
|
||||
layout <modo> master-stack · centered-master · spiral
|
||||
grid · columns · rows · monocle
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
//! ```text
|
||||
//! n abre una ventana tab / espacio cicla layout
|
||||
//! w cierra la enfocada t m g c r d s layout directo
|
||||
//! f flota / tesela h / l área maestra −/+
|
||||
//! f / Shift+f flota / pantalla completa h / l área maestra −/+
|
||||
//! j / k foco siguiente/anterior , / . nmaster −/+
|
||||
//! Shift+j / k mueve la enfocada 1..9 ir a escritorio
|
||||
//! Enter promueve a maestra Ctrl+1..9 enviar a escritorio
|
||||
@@ -283,6 +283,7 @@ impl Mirada {
|
||||
match ks.key.as_str() {
|
||||
"n" if !connected => self.open_window(),
|
||||
"w" => self.act(DesktopAction::CloseFocused),
|
||||
"f" if shift => self.act(DesktopAction::ToggleFullscreen),
|
||||
"f" => self.act(DesktopAction::ToggleFloat),
|
||||
"j" if shift => self.act(DesktopAction::MoveForward),
|
||||
"k" if shift => self.act(DesktopAction::MoveBackward),
|
||||
@@ -454,7 +455,9 @@ impl Render for Mirada {
|
||||
let tb_bg = if p.focused { theme.accent } else { theme.bg_row_hover };
|
||||
let tb_fg = if p.focused { on_accent } else { theme.fg_muted };
|
||||
let pid = p.id;
|
||||
let kind_label = if p.floating {
|
||||
let kind_label = if p.fullscreen {
|
||||
"· pantalla completa ·"
|
||||
} else if p.floating {
|
||||
"· ventana flotante ·"
|
||||
} else {
|
||||
"· superficie del Cuerpo ·"
|
||||
|
||||
@@ -118,6 +118,10 @@ que un front-end (`Keybind` → `lookup` → `apply`); hay otros tres:
|
||||
flotantes aparte, `layout()` las pone al final y `WindowPlacement`
|
||||
/`BodyOp::Configure` llevan `floating: bool` para que el Cuerpo las
|
||||
componga por encima.
|
||||
- **Pantalla completa** — `ToggleFullscreen` (`Super+Shift+f`): la ventana
|
||||
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
|
||||
`xdg_toplevel Fullscreen`.
|
||||
- **Layout y área maestra por el API** — los 7 modos se intercambian
|
||||
(`SetLayout`/`CycleLayout`, `mirada-ctl layout spiral`); el área
|
||||
maestra se redimensiona (`grow`/`shrink-master`, `Super+l`/`Super+h`);
|
||||
@@ -160,7 +164,7 @@ a las ya abiertas.
|
||||
## Estado
|
||||
|
||||
Implementado y verde: `mirada-layout` (32 tests), `mirada-protocol`
|
||||
(10), `mirada-brain` (51), `mirada-link` (7), `mirada-body` (14), las
|
||||
(11), `mirada-brain` (52), `mirada-link` (7), `mirada-body` (14), las
|
||||
apps `mirada` y `mirada-compositor` (compilan; verificación visual
|
||||
manual) y `mirada-ctl` (CLI, probado vía el ejemplo `headless-ctl`).
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ pub struct Surface {
|
||||
pub focused: bool,
|
||||
/// `true` si flota: el backend la pinta por encima de las teseladas.
|
||||
pub floating: bool,
|
||||
/// `true` si está en pantalla completa.
|
||||
pub fullscreen: bool,
|
||||
}
|
||||
|
||||
impl Surface {
|
||||
@@ -42,6 +44,7 @@ impl Surface {
|
||||
visible: false,
|
||||
focused: false,
|
||||
floating: false,
|
||||
fullscreen: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,9 +52,16 @@ impl Surface {
|
||||
/// Una orden concreta para el backend (smithay, headless, …).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum BodyOp {
|
||||
/// Recoloca una superficie, la muestra u oculta y dice si flota
|
||||
/// (las flotantes se componen por encima de las teseladas).
|
||||
Configure { id: WindowId, rect: Rect, visible: bool, floating: bool },
|
||||
/// Recoloca una superficie, la muestra u oculta y dice si flota o
|
||||
/// está en pantalla completa (el backend ajusta el orden de pintado
|
||||
/// y el estado `xdg_toplevel` en consecuencia).
|
||||
Configure {
|
||||
id: WindowId,
|
||||
rect: Rect,
|
||||
visible: bool,
|
||||
floating: bool,
|
||||
fullscreen: bool,
|
||||
},
|
||||
/// Da el foco del teclado a una superficie.
|
||||
Focus(WindowId),
|
||||
/// Quita el foco a todas las superficies.
|
||||
@@ -104,15 +114,18 @@ impl BodyState {
|
||||
if s.geometry != Some(p.rect)
|
||||
|| s.visible != p.visible
|
||||
|| s.floating != p.floating
|
||||
|| s.fullscreen != p.fullscreen
|
||||
{
|
||||
s.geometry = Some(p.rect);
|
||||
s.visible = p.visible;
|
||||
s.floating = p.floating;
|
||||
s.fullscreen = p.fullscreen;
|
||||
ops.push(BodyOp::Configure {
|
||||
id: p.id,
|
||||
rect: p.rect,
|
||||
visible: p.visible,
|
||||
floating: p.floating,
|
||||
fullscreen: p.fullscreen,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -128,6 +141,7 @@ impl BodyState {
|
||||
rect,
|
||||
visible: false,
|
||||
floating: s.floating,
|
||||
fullscreen: s.fullscreen,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -248,6 +262,7 @@ mod tests {
|
||||
visible,
|
||||
focused,
|
||||
floating: false,
|
||||
fullscreen: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +325,7 @@ mod tests {
|
||||
rect: Rect::new(0, 0, 800, 600),
|
||||
visible: false,
|
||||
floating: false,
|
||||
fullscreen: false,
|
||||
}));
|
||||
assert!(!b.surface(2).unwrap().visible);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,13 @@ fn main() {
|
||||
p.rect.h,
|
||||
p.rect.x,
|
||||
p.rect.y,
|
||||
if p.floating { " ~flotante" } else { "" },
|
||||
if p.fullscreen {
|
||||
" ~pantalla"
|
||||
} else if p.floating {
|
||||
" ~flotante"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if p.focused { " *" } else { "" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ pub enum DesktopAction {
|
||||
CloseFocused,
|
||||
/// Alterna entre flotante y teselada la ventana enfocada.
|
||||
ToggleFloat,
|
||||
/// Alterna pantalla completa en la ventana enfocada.
|
||||
ToggleFullscreen,
|
||||
/// Pasa al siguiente modo de teselado.
|
||||
CycleLayout,
|
||||
/// Fija un modo de teselado concreto.
|
||||
@@ -101,6 +103,7 @@ impl fmt::Display for DesktopAction {
|
||||
DesktopAction::MoveBackward => f.write_str("move-backward"),
|
||||
DesktopAction::CloseFocused => f.write_str("close-focused"),
|
||||
DesktopAction::ToggleFloat => f.write_str("toggle-float"),
|
||||
DesktopAction::ToggleFullscreen => f.write_str("toggle-fullscreen"),
|
||||
DesktopAction::CycleLayout => f.write_str("cycle-layout"),
|
||||
DesktopAction::SetLayout(m) => write!(f, "layout:{}", layout_slug(*m)),
|
||||
DesktopAction::GrowMaster => f.write_str("grow-master"),
|
||||
@@ -129,6 +132,7 @@ impl FromStr for DesktopAction {
|
||||
"move-backward" => Self::MoveBackward,
|
||||
"close-focused" => Self::CloseFocused,
|
||||
"toggle-float" => Self::ToggleFloat,
|
||||
"toggle-fullscreen" => Self::ToggleFullscreen,
|
||||
"cycle-layout" => Self::CycleLayout,
|
||||
"grow-master" => Self::GrowMaster,
|
||||
"shrink-master" => Self::ShrinkMaster,
|
||||
@@ -188,6 +192,7 @@ pub fn default_keymap() -> Vec<(String, DesktopAction)> {
|
||||
("Super+Shift+k".into(), DesktopAction::MoveBackward),
|
||||
("Super+q".into(), DesktopAction::CloseFocused),
|
||||
("Super+f".into(), DesktopAction::ToggleFloat),
|
||||
("Super+Shift+f".into(), DesktopAction::ToggleFullscreen),
|
||||
("Super+space".into(), DesktopAction::CycleLayout),
|
||||
("Super+t".into(), DesktopAction::SetLayout(LayoutMode::MasterStack)),
|
||||
("Super+m".into(), DesktopAction::SetLayout(LayoutMode::Monocle)),
|
||||
|
||||
@@ -218,6 +218,18 @@ impl Desktop {
|
||||
}
|
||||
self.relayout()
|
||||
}
|
||||
DesktopAction::ToggleFullscreen => {
|
||||
let Some(id) = self.workspaces[self.active].focused() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let ws = &mut self.workspaces[self.active];
|
||||
if ws.fullscreen() == Some(id) {
|
||||
ws.set_fullscreen(None);
|
||||
} else {
|
||||
ws.set_fullscreen(Some(id));
|
||||
}
|
||||
self.relayout()
|
||||
}
|
||||
DesktopAction::CycleLayout => {
|
||||
let next = self.workspaces[self.active].params().mode.next();
|
||||
self.workspaces[self.active].set_mode(next);
|
||||
@@ -475,6 +487,23 @@ mod tests {
|
||||
assert!(!places(&cmds).iter().find(|x| x.id == 2).unwrap().floating);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_fullscreen_covers_the_screen_and_hides_the_rest() {
|
||||
let mut d = desktop_with_screen();
|
||||
for id in [1, 2, 3] {
|
||||
open(&mut d, id);
|
||||
}
|
||||
let cmds = d.apply(DesktopAction::ToggleFullscreen); // sobre la 3
|
||||
let p = places(&cmds);
|
||||
let fs = p.iter().find(|x| x.id == 3).unwrap();
|
||||
assert!(fs.fullscreen && fs.visible);
|
||||
assert_eq!(fs.rect, d.screen().unwrap());
|
||||
assert!(p.iter().filter(|x| x.id != 3).all(|x| !x.visible));
|
||||
// Alternar de nuevo restaura el teselado: las tres visibles.
|
||||
let cmds = d.apply(DesktopAction::ToggleFullscreen);
|
||||
assert_eq!(places(&cmds).iter().filter(|x| x.visible).count(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rule_sends_a_new_window_to_its_workspace() {
|
||||
let mut d = desktop_with_screen();
|
||||
|
||||
@@ -238,6 +238,7 @@ const KEYMAP_HEADER: &str = "\
|
||||
// move-forward / move-backward reordena la ventana enfocada
|
||||
// close-focused cierra la enfocada
|
||||
// toggle-float alterna flotante / teselada
|
||||
// toggle-fullscreen alterna pantalla completa
|
||||
// cycle-layout siguiente modo de teselado
|
||||
// layout:<modo> master-stack | centered-master | spiral
|
||||
// grid | columns | rows | monocle
|
||||
|
||||
@@ -21,6 +21,9 @@ pub struct Workspace {
|
||||
/// Ventanas flotantes y su rectángulo: salen del teselado y se pintan
|
||||
/// encima. Las que no están aquí se teselan normalmente.
|
||||
floating: BTreeMap<WindowId, Rect>,
|
||||
/// La ventana en pantalla completa, si hay alguna: cubre toda la
|
||||
/// salida y oculta al resto.
|
||||
fullscreen: Option<WindowId>,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
@@ -31,6 +34,7 @@ impl Workspace {
|
||||
focus: 0,
|
||||
params,
|
||||
floating: BTreeMap::new(),
|
||||
fullscreen: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +88,9 @@ impl Workspace {
|
||||
};
|
||||
self.windows.remove(i);
|
||||
self.floating.remove(&window);
|
||||
if self.fullscreen == Some(window) {
|
||||
self.fullscreen = None;
|
||||
}
|
||||
if i < self.focus {
|
||||
self.focus -= 1;
|
||||
}
|
||||
@@ -111,6 +118,16 @@ impl Workspace {
|
||||
self.floating.contains_key(&window)
|
||||
}
|
||||
|
||||
/// La ventana en pantalla completa de este escritorio, si hay alguna.
|
||||
pub fn fullscreen(&self) -> Option<WindowId> {
|
||||
self.fullscreen
|
||||
}
|
||||
|
||||
/// Pone (o quita, con `None`) la ventana en pantalla completa.
|
||||
pub fn set_fullscreen(&mut self, window: Option<WindowId>) {
|
||||
self.fullscreen = window;
|
||||
}
|
||||
|
||||
/// Ventana enfocada, o `None` si el escritorio está vacío.
|
||||
pub fn focused(&self) -> Option<WindowId> {
|
||||
self.windows.get(self.focus).copied()
|
||||
|
||||
@@ -48,6 +48,8 @@ pub struct WindowPlacement {
|
||||
pub focused: bool,
|
||||
/// `true` si flota (fuera del teselado): el Cuerpo la pinta encima.
|
||||
pub floating: bool,
|
||||
/// `true` si está en pantalla completa: cubre toda la salida.
|
||||
pub fullscreen: bool,
|
||||
}
|
||||
|
||||
/// Una orden del Cerebro al Cuerpo.
|
||||
@@ -140,20 +142,31 @@ pub fn read_frame<R: Read, T: DeserializeOwned>(r: &mut R) -> io::Result<Option<
|
||||
/// En modo [`LayoutMode::Monocle`] sólo la ventana enfocada queda
|
||||
/// `visible`; en el resto de modos todas lo están.
|
||||
pub fn placements(ws: &Workspace, screen: Rect) -> Vec<WindowPlacement> {
|
||||
let fullscreen = ws.fullscreen();
|
||||
let monocle = ws.params().mode == LayoutMode::Monocle;
|
||||
let focused = ws.focused();
|
||||
ws.layout(screen)
|
||||
.into_iter()
|
||||
.map(|(id, rect)| {
|
||||
let is_focused = focused == Some(id);
|
||||
let floating = ws.is_floating(id);
|
||||
let is_fs = fullscreen == Some(id);
|
||||
// Con una ventana en pantalla completa manda ella: ocupa toda
|
||||
// la salida, es la única visible y se lleva el foco.
|
||||
let (rect, visible, is_focused) = match fullscreen {
|
||||
Some(_) => (if is_fs { screen } else { rect }, is_fs, is_fs),
|
||||
None => {
|
||||
let f = focused == Some(id);
|
||||
// Una flotante siempre se ve; en `Monocle`, sólo la enfocada.
|
||||
(rect, floating || !monocle || f, f)
|
||||
}
|
||||
};
|
||||
WindowPlacement {
|
||||
id,
|
||||
rect,
|
||||
// Una flotante siempre se ve; en `Monocle`, sólo la enfocada.
|
||||
visible: floating || !monocle || is_focused,
|
||||
visible,
|
||||
focused: is_focused,
|
||||
floating,
|
||||
fullscreen: is_fs,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -183,6 +196,7 @@ mod tests {
|
||||
visible: true,
|
||||
focused: true,
|
||||
floating: false,
|
||||
fullscreen: false,
|
||||
}]);
|
||||
let mut buf = Vec::new();
|
||||
write_frame(&mut buf, &cmd).unwrap();
|
||||
@@ -277,6 +291,19 @@ mod tests {
|
||||
assert_eq!(f.rect, Rect::new(0, 0, 200, 200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fullscreen_window_covers_the_screen_and_hides_the_rest() {
|
||||
let mut w = ws(LayoutMode::Columns);
|
||||
w.set_fullscreen(Some(20));
|
||||
let p = placements(&w, SCREEN);
|
||||
let fs = p.iter().find(|x| x.id == 20).unwrap();
|
||||
assert!(fs.fullscreen);
|
||||
assert!(fs.focused, "la ventana en pantalla completa se lleva el foco");
|
||||
assert_eq!(fs.rect, SCREEN);
|
||||
// El resto queda oculto.
|
||||
assert!(p.iter().filter(|x| x.id != 20).all(|x| !x.visible));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placements_fill_a_place_command_round_trip() {
|
||||
let cmd = BrainCommand::Place(placements(&ws(LayoutMode::Grid), SCREEN));
|
||||
|
||||
Reference in New Issue
Block a user