feat(mirada): multi-monitor real — cada salida tesela su escritorio
El Desktop deja de teselar sólo la salida primaria. Cada Output muestra
un escritorio virtual distinto y relayout() las tesela todas en un solo
Place que cubre todas las pantallas.
- Output { id, rect, workspace }; focused_output reemplaza al índice
global active. active_index() = el escritorio de la salida enfocada.
- OutputAdded asigna el primer escritorio libre; OutputRemoved deja sus
ventanas en su escritorio y reajusta el foco. reflow_outputs() las
recoloca en fila.
- SwitchWorkspace actúa sobre la salida enfocada; si el escritorio
pedido ya lo muestra otra salida, las intercambia (invariante: un
escritorio se ve en una salida como mucho).
- DesktopAction::FocusOutputNext (Super+o) mueve el foco entre
monitores. El foco del teclado es único — relayout() lo unifica a la
ventana enfocada de la salida enfocada.
Verificado end-to-end con headless-ctl (ahora 2 salidas).
mirada-brain 52->58 tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,7 @@ Acciones de mirada-ctl:
|
|||||||
promote-to-master la ventana enfocada al puesto maestro
|
promote-to-master la ventana enfocada al puesto maestro
|
||||||
workspace <n> activa el escritorio n (1..9)
|
workspace <n> activa el escritorio n (1..9)
|
||||||
send-to-workspace <n> manda la enfocada al escritorio n
|
send-to-workspace <n> manda la enfocada al escritorio n
|
||||||
|
focus-output-next pasa el foco al siguiente monitor
|
||||||
quit apaga el compositor
|
quit apaga el compositor
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
//! j / k foco siguiente/anterior , / . nmaster −/+
|
//! j / k foco siguiente/anterior , / . nmaster −/+
|
||||||
//! Shift+j / k mueve la enfocada 1..9 ir a escritorio
|
//! Shift+j / k mueve la enfocada 1..9 ir a escritorio
|
||||||
//! Enter promueve a maestra Ctrl+1..9 enviar a escritorio
|
//! Enter promueve a maestra Ctrl+1..9 enviar a escritorio
|
||||||
|
//! o siguiente monitor
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Los pips de escritorio y las ventanas del lienzo son **clicables**, y
|
//! Los pips de escritorio y las ventanas del lienzo son **clicables**, y
|
||||||
@@ -299,6 +300,7 @@ impl Mirada {
|
|||||||
"s" => self.act(DesktopAction::SetLayout(LayoutMode::Spiral)),
|
"s" => self.act(DesktopAction::SetLayout(LayoutMode::Spiral)),
|
||||||
"h" => self.act(DesktopAction::ShrinkMaster),
|
"h" => self.act(DesktopAction::ShrinkMaster),
|
||||||
"l" => self.act(DesktopAction::GrowMaster),
|
"l" => self.act(DesktopAction::GrowMaster),
|
||||||
|
"o" => self.act(DesktopAction::FocusOutputNext),
|
||||||
"enter" => self.act(DesktopAction::PromoteToMaster),
|
"enter" => self.act(DesktopAction::PromoteToMaster),
|
||||||
"," => self.act(DesktopAction::IncMaster),
|
"," => self.act(DesktopAction::IncMaster),
|
||||||
"." => self.act(DesktopAction::DecMaster),
|
"." => self.act(DesktopAction::DecMaster),
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ ejecuta operaciones de geometría".
|
|||||||
- **`mirada-brain`** — `Desktop`: salidas, 9 escritorios virtuales,
|
- **`mirada-brain`** — `Desktop`: salidas, 9 escritorios virtuales,
|
||||||
registro de ventanas. `on_event(BodyEvent) -> Vec<BrainCommand>`;
|
registro de ventanas. `on_event(BodyEvent) -> Vec<BrainCommand>`;
|
||||||
`DesktopAction` + `Keymap` configurable (`set_keymap` en caliente) +
|
`DesktopAction` + `Keymap` configurable (`set_keymap` en caliente) +
|
||||||
`Rules` de ventana.
|
`Rules` de ventana. **Multi-monitor**: cada `Output` muestra un
|
||||||
|
escritorio; `relayout()` tesela todas las salidas en un solo `Place`.
|
||||||
- **`mirada-link`** — `Link<Out,In>` sobre socket Unix; hilo lector de
|
- **`mirada-link`** — `Link<Out,In>` sobre socket Unix; hilo lector de
|
||||||
fondo + canal `mpsc` para sondeo no bloqueante. `BrainLink`/`BodyLink`,
|
fondo + canal `mpsc` para sondeo no bloqueante. `BrainLink`/`BodyLink`,
|
||||||
`connected_pair` (socketpair), `connect`/`listen` por ruta.
|
`connected_pair` (socketpair), `connect`/`listen` por ruta.
|
||||||
@@ -122,6 +123,11 @@ que un front-end (`Keybind` → `lookup` → `apply`); hay otros tres:
|
|||||||
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`.
|
||||||
|
- **Multi-monitor** — cada `Output` muestra un escritorio distinto;
|
||||||
|
`SwitchWorkspace` actúa sobre la salida enfocada (y la intercambia si
|
||||||
|
el escritorio pedido ya lo muestra otra salida); `FocusOutputNext`
|
||||||
|
(`Super+o`) mueve el foco entre monitores. El foco del teclado es
|
||||||
|
único — sólo la ventana enfocada de la salida enfocada.
|
||||||
- **Layout y área maestra por el API** — los 7 modos se intercambian
|
- **Layout y área maestra por el API** — los 7 modos se intercambian
|
||||||
(`SetLayout`/`CycleLayout`, `mirada-ctl layout spiral`); el área
|
(`SetLayout`/`CycleLayout`, `mirada-ctl layout spiral`); el área
|
||||||
maestra se redimensiona (`grow`/`shrink-master`, `Super+l`/`Super+h`);
|
maestra se redimensiona (`grow`/`shrink-master`, `Super+l`/`Super+h`);
|
||||||
@@ -164,7 +170,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` (52), `mirada-link` (7), `mirada-body` (14), las
|
(11), `mirada-brain` (58), `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`).
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ fn main() {
|
|||||||
};
|
};
|
||||||
eprintln!("Cerebro headless · control en {}", path.display());
|
eprintln!("Cerebro headless · control en {}", path.display());
|
||||||
|
|
||||||
// Una pantalla y tres ventanas de muestra.
|
// Dos pantallas y tres ventanas de muestra.
|
||||||
let mut desktop = Desktop::new();
|
let mut desktop = Desktop::new();
|
||||||
desktop.on_event(BodyEvent::OutputAdded { id: 0, width: 1920, height: 1080 });
|
desktop.on_event(BodyEvent::OutputAdded { id: 0, width: 1920, height: 1080 });
|
||||||
|
desktop.on_event(BodyEvent::OutputAdded { id: 1, width: 1920, height: 1080 });
|
||||||
for id in 1..=3 {
|
for id in 1..=3 {
|
||||||
desktop.on_event(BodyEvent::WindowOpened {
|
desktop.on_event(BodyEvent::WindowOpened {
|
||||||
id,
|
id,
|
||||||
@@ -93,10 +94,21 @@ fn main() {
|
|||||||
fn print_state(d: &Desktop) {
|
fn print_state(d: &Desktop) {
|
||||||
let ws = d.active_workspace();
|
let ws = d.active_workspace();
|
||||||
eprintln!(
|
eprintln!(
|
||||||
" escritorio {} · {:?} (maestra {:.0}%) · foco {:?}",
|
" activo: escritorio {} · {:?} (maestra {:.0}%) · foco {:?}",
|
||||||
d.active_index() + 1,
|
d.active_index() + 1,
|
||||||
ws.params().mode,
|
ws.params().mode,
|
||||||
ws.params().master_ratio * 100.0,
|
ws.params().master_ratio * 100.0,
|
||||||
d.focused_window(),
|
d.focused_window(),
|
||||||
);
|
);
|
||||||
|
for (i, o) in d.outputs().iter().enumerate() {
|
||||||
|
let mark = if i == d.focused_output() { '*' } else { ' ' };
|
||||||
|
eprintln!(
|
||||||
|
" {mark} salida {} {}×{} @ x{} → escritorio {}",
|
||||||
|
o.id,
|
||||||
|
o.rect.w,
|
||||||
|
o.rect.h,
|
||||||
|
o.rect.x,
|
||||||
|
o.workspace + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ pub enum DesktopAction {
|
|||||||
SwitchWorkspace(usize),
|
SwitchWorkspace(usize),
|
||||||
/// Manda la ventana enfocada al escritorio virtual `n`.
|
/// Manda la ventana enfocada al escritorio virtual `n`.
|
||||||
SendToWorkspace(usize),
|
SendToWorkspace(usize),
|
||||||
|
/// Mueve el foco a la siguiente salida (monitor).
|
||||||
|
FocusOutputNext,
|
||||||
/// Apaga el compositor.
|
/// Apaga el compositor.
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
@@ -114,6 +116,7 @@ impl fmt::Display for DesktopAction {
|
|||||||
// Los escritorios se numeran 1-based de cara al usuario.
|
// Los escritorios se numeran 1-based de cara al usuario.
|
||||||
DesktopAction::SwitchWorkspace(n) => write!(f, "workspace:{}", n + 1),
|
DesktopAction::SwitchWorkspace(n) => write!(f, "workspace:{}", n + 1),
|
||||||
DesktopAction::SendToWorkspace(n) => write!(f, "send-to-workspace:{}", n + 1),
|
DesktopAction::SendToWorkspace(n) => write!(f, "send-to-workspace:{}", n + 1),
|
||||||
|
DesktopAction::FocusOutputNext => f.write_str("focus-output-next"),
|
||||||
DesktopAction::Quit => f.write_str("quit"),
|
DesktopAction::Quit => f.write_str("quit"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,6 +142,7 @@ impl FromStr for DesktopAction {
|
|||||||
"inc-master" => Self::IncMaster,
|
"inc-master" => Self::IncMaster,
|
||||||
"dec-master" => Self::DecMaster,
|
"dec-master" => Self::DecMaster,
|
||||||
"promote-to-master" => Self::PromoteToMaster,
|
"promote-to-master" => Self::PromoteToMaster,
|
||||||
|
"focus-output-next" => Self::FocusOutputNext,
|
||||||
"quit" => Self::Quit,
|
"quit" => Self::Quit,
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(slug) = s.strip_prefix("layout:") {
|
if let Some(slug) = s.strip_prefix("layout:") {
|
||||||
@@ -203,6 +207,7 @@ pub fn default_keymap() -> Vec<(String, DesktopAction)> {
|
|||||||
("Super+s".into(), DesktopAction::SetLayout(LayoutMode::Spiral)),
|
("Super+s".into(), DesktopAction::SetLayout(LayoutMode::Spiral)),
|
||||||
("Super+h".into(), DesktopAction::ShrinkMaster),
|
("Super+h".into(), DesktopAction::ShrinkMaster),
|
||||||
("Super+l".into(), DesktopAction::GrowMaster),
|
("Super+l".into(), DesktopAction::GrowMaster),
|
||||||
|
("Super+o".into(), DesktopAction::FocusOutputNext),
|
||||||
("Super+Return".into(), DesktopAction::PromoteToMaster),
|
("Super+Return".into(), DesktopAction::PromoteToMaster),
|
||||||
("Super+,".into(), DesktopAction::IncMaster),
|
("Super+,".into(), DesktopAction::IncMaster),
|
||||||
("Super+.".into(), DesktopAction::DecMaster),
|
("Super+.".into(), DesktopAction::DecMaster),
|
||||||
|
|||||||
@@ -16,22 +16,34 @@ pub struct WindowInfo {
|
|||||||
pub title: String,
|
pub title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Una salida física y el escritorio virtual que muestra ahora mismo.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Output {
|
||||||
|
pub id: OutputId,
|
||||||
|
/// Rectángulo en el espacio global — las salidas van en fila horizontal.
|
||||||
|
pub rect: Rect,
|
||||||
|
/// Índice del escritorio que esta salida muestra.
|
||||||
|
pub workspace: usize,
|
||||||
|
}
|
||||||
|
|
||||||
/// El estado completo del escritorio.
|
/// El estado completo del escritorio.
|
||||||
///
|
///
|
||||||
/// Mantiene las salidas físicas, [`WORKSPACE_COUNT`] escritorios
|
/// Mantiene las salidas físicas, [`WORKSPACE_COUNT`] escritorios
|
||||||
/// virtuales, el registro de ventanas y el mapa de atajos. El único
|
/// virtuales, el registro de ventanas, el keymap y las reglas. El único
|
||||||
/// punto de entrada es [`Desktop::on_event`]: traga un [`BodyEvent`],
|
/// punto de entrada es [`Desktop::on_event`]: traga un [`BodyEvent`],
|
||||||
/// muta el estado y devuelve los [`BrainCommand`]s a enviar al Cuerpo.
|
/// muta el estado y devuelve los [`BrainCommand`]s a enviar al Cuerpo.
|
||||||
///
|
///
|
||||||
/// Limitación de v1: el teselado se calcula sobre la salida primaria
|
/// **Multi-monitor**: cada salida muestra un escritorio distinto; el
|
||||||
/// (la primera conectada). El multi-monitor real llegará después.
|
/// teselado se calcula para todas y el `Place` resultante las cubre. Un
|
||||||
|
/// escritorio se ve en una salida como mucho — pedir uno que ya muestra
|
||||||
|
/// otra salida las intercambia.
|
||||||
pub struct Desktop {
|
pub struct Desktop {
|
||||||
/// Salidas físicas, en fila horizontal y en orden de aparición.
|
/// Salidas físicas, en fila horizontal y en orden de aparición.
|
||||||
outputs: Vec<(OutputId, Rect)>,
|
outputs: Vec<Output>,
|
||||||
/// Escritorios virtuales — `WORKSPACE_COUNT` fijos.
|
/// Escritorios virtuales — `WORKSPACE_COUNT` fijos.
|
||||||
workspaces: Vec<Workspace>,
|
workspaces: Vec<Workspace>,
|
||||||
/// Índice del escritorio activo.
|
/// Índice (en `outputs`) de la salida con el foco.
|
||||||
active: usize,
|
focused_output: usize,
|
||||||
/// Identidad de cada ventana conocida.
|
/// Identidad de cada ventana conocida.
|
||||||
windows: HashMap<WindowId, WindowInfo>,
|
windows: HashMap<WindowId, WindowInfo>,
|
||||||
/// Atajos globales → acción. Configurable, recargable en caliente.
|
/// Atajos globales → acción. Configurable, recargable en caliente.
|
||||||
@@ -62,7 +74,7 @@ impl Desktop {
|
|||||||
Self {
|
Self {
|
||||||
outputs: Vec::new(),
|
outputs: Vec::new(),
|
||||||
workspaces,
|
workspaces,
|
||||||
active: 0,
|
focused_output: 0,
|
||||||
windows: HashMap::new(),
|
windows: HashMap::new(),
|
||||||
keymap,
|
keymap,
|
||||||
rules: Rules::default(),
|
rules: Rules::default(),
|
||||||
@@ -93,9 +105,9 @@ impl Desktop {
|
|||||||
&self.keymap
|
&self.keymap
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Geometría de la salida primaria, si hay alguna conectada.
|
/// Geometría de la salida enfocada, si hay alguna conectada.
|
||||||
pub fn screen(&self) -> Option<Rect> {
|
pub fn screen(&self) -> Option<Rect> {
|
||||||
self.outputs.first().map(|(_, r)| *r)
|
self.outputs.get(self.focused_output).map(|o| o.rect)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Procesa un evento del Cuerpo: muta el estado y devuelve los
|
/// Procesa un evento del Cuerpo: muta el estado y devuelve los
|
||||||
@@ -103,13 +115,26 @@ impl Desktop {
|
|||||||
pub fn on_event(&mut self, event: BodyEvent) -> Vec<BrainCommand> {
|
pub fn on_event(&mut self, event: BodyEvent) -> Vec<BrainCommand> {
|
||||||
match event {
|
match event {
|
||||||
BodyEvent::OutputAdded { id, width, height } => {
|
BodyEvent::OutputAdded { id, width, height } => {
|
||||||
// Las salidas se alinean en fila a la derecha de las previas.
|
// La salida nueva muestra el primer escritorio que no
|
||||||
let x: i32 = self.outputs.iter().map(|(_, r)| r.w).sum();
|
// muestre ya otra salida.
|
||||||
self.outputs.push((id, Rect::new(x, 0, width, height)));
|
let taken: Vec<usize> = self.outputs.iter().map(|o| o.workspace).collect();
|
||||||
|
let workspace = (0..self.workspaces.len())
|
||||||
|
.find(|n| !taken.contains(n))
|
||||||
|
.unwrap_or(0);
|
||||||
|
self.outputs.push(Output {
|
||||||
|
id,
|
||||||
|
rect: Rect::new(0, 0, width, height),
|
||||||
|
workspace,
|
||||||
|
});
|
||||||
|
self.reflow_outputs();
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
BodyEvent::OutputRemoved { id } => {
|
BodyEvent::OutputRemoved { id } => {
|
||||||
self.outputs.retain(|(o, _)| *o != id);
|
self.outputs.retain(|o| o.id != id);
|
||||||
|
if self.focused_output >= self.outputs.len() {
|
||||||
|
self.focused_output = self.outputs.len().saturating_sub(1);
|
||||||
|
}
|
||||||
|
self.reflow_outputs();
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
BodyEvent::WindowOpened { id, app_id, title } => {
|
BodyEvent::WindowOpened { id, app_id, title } => {
|
||||||
@@ -119,7 +144,7 @@ impl Desktop {
|
|||||||
let ws = outcome
|
let ws = outcome
|
||||||
.workspace
|
.workspace
|
||||||
.filter(|&n| n < self.workspaces.len())
|
.filter(|&n| n < self.workspaces.len())
|
||||||
.unwrap_or(self.active);
|
.unwrap_or(self.active_index());
|
||||||
self.workspaces[ws].add(id);
|
self.workspaces[ws].add(id);
|
||||||
if outcome.floating {
|
if outcome.floating {
|
||||||
let rect = self
|
let rect = self
|
||||||
@@ -147,7 +172,8 @@ impl Desktop {
|
|||||||
BodyEvent::PointerEntered { id } => {
|
BodyEvent::PointerEntered { id } => {
|
||||||
// Foco al pasar el puntero, sólo si la ventana está en el
|
// Foco al pasar el puntero, sólo si la ventana está en el
|
||||||
// escritorio activo.
|
// escritorio activo.
|
||||||
if self.workspaces[self.active].focus_window(id) {
|
let active = self.active_index();
|
||||||
|
if self.workspaces[active].focus_window(id) {
|
||||||
self.relayout()
|
self.relayout()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
@@ -163,51 +189,52 @@ impl Desktop {
|
|||||||
/// Aplica una acción de escritorio directamente (sin pasar por una
|
/// Aplica una acción de escritorio directamente (sin pasar por una
|
||||||
/// tecla). Útil para disparar acciones desde un HUD.
|
/// tecla). Útil para disparar acciones desde un HUD.
|
||||||
pub fn apply(&mut self, action: DesktopAction) -> Vec<BrainCommand> {
|
pub fn apply(&mut self, action: DesktopAction) -> Vec<BrainCommand> {
|
||||||
|
let active = self.active_index();
|
||||||
match action {
|
match action {
|
||||||
DesktopAction::FocusNext => {
|
DesktopAction::FocusNext => {
|
||||||
self.workspaces[self.active].focus_next();
|
self.workspaces[active].focus_next();
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
DesktopAction::FocusPrev => {
|
DesktopAction::FocusPrev => {
|
||||||
self.workspaces[self.active].focus_prev();
|
self.workspaces[active].focus_prev();
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
DesktopAction::FocusWindow(id) => {
|
DesktopAction::FocusWindow(id) => {
|
||||||
// En el escritorio activo basta enfocar; si la ventana
|
// En el escritorio activo basta enfocar; si la ventana
|
||||||
// está en otro, saltamos a ese escritorio.
|
// está en otro, lo traemos a la salida enfocada.
|
||||||
if self.workspaces[self.active].focus_window(id) {
|
if self.workspaces[active].focus_window(id) {
|
||||||
return self.relayout();
|
return self.relayout();
|
||||||
}
|
}
|
||||||
for n in 0..self.workspaces.len() {
|
for n in 0..self.workspaces.len() {
|
||||||
if n != self.active && self.workspaces[n].focus_window(id) {
|
if n != active && self.workspaces[n].focus_window(id) {
|
||||||
self.active = n;
|
self.show_workspace(n);
|
||||||
return self.relayout();
|
return self.relayout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
DesktopAction::MoveForward => {
|
DesktopAction::MoveForward => {
|
||||||
self.workspaces[self.active].move_focused_forward();
|
self.workspaces[active].move_focused_forward();
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
DesktopAction::MoveBackward => {
|
DesktopAction::MoveBackward => {
|
||||||
self.workspaces[self.active].move_focused_backward();
|
self.workspaces[active].move_focused_backward();
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
DesktopAction::CloseFocused => {
|
DesktopAction::CloseFocused => {
|
||||||
// Pedimos el cierre; el estado se actualiza al recibir el
|
// Pedimos el cierre; el estado se actualiza al recibir el
|
||||||
// `WindowClosed` de vuelta, no antes.
|
// `WindowClosed` de vuelta, no antes.
|
||||||
match self.workspaces[self.active].focused() {
|
match self.workspaces[active].focused() {
|
||||||
Some(id) => vec![BrainCommand::Close(id)],
|
Some(id) => vec![BrainCommand::Close(id)],
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DesktopAction::ToggleFloat => {
|
DesktopAction::ToggleFloat => {
|
||||||
let Some(id) = self.workspaces[self.active].focused() else {
|
let Some(id) = self.workspaces[active].focused() else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let screen = self.screen();
|
let screen = self.screen();
|
||||||
let ws = &mut self.workspaces[self.active];
|
let ws = &mut self.workspaces[active];
|
||||||
if ws.is_floating(id) {
|
if ws.is_floating(id) {
|
||||||
ws.set_floating(id, None);
|
ws.set_floating(id, None);
|
||||||
} else {
|
} else {
|
||||||
@@ -219,10 +246,10 @@ impl Desktop {
|
|||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
DesktopAction::ToggleFullscreen => {
|
DesktopAction::ToggleFullscreen => {
|
||||||
let Some(id) = self.workspaces[self.active].focused() else {
|
let Some(id) = self.workspaces[active].focused() else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let ws = &mut self.workspaces[self.active];
|
let ws = &mut self.workspaces[active];
|
||||||
if ws.fullscreen() == Some(id) {
|
if ws.fullscreen() == Some(id) {
|
||||||
ws.set_fullscreen(None);
|
ws.set_fullscreen(None);
|
||||||
} else {
|
} else {
|
||||||
@@ -231,12 +258,12 @@ impl Desktop {
|
|||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
DesktopAction::CycleLayout => {
|
DesktopAction::CycleLayout => {
|
||||||
let next = self.workspaces[self.active].params().mode.next();
|
let next = self.workspaces[active].params().mode.next();
|
||||||
self.workspaces[self.active].set_mode(next);
|
self.workspaces[active].set_mode(next);
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
DesktopAction::SetLayout(mode) => {
|
DesktopAction::SetLayout(mode) => {
|
||||||
self.workspaces[self.active].set_mode(mode);
|
self.workspaces[active].set_mode(mode);
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
DesktopAction::GrowMaster => self.nudge_master(0.05),
|
DesktopAction::GrowMaster => self.nudge_master(0.05),
|
||||||
@@ -244,38 +271,83 @@ impl Desktop {
|
|||||||
DesktopAction::IncMaster => self.nudge_master_count(1),
|
DesktopAction::IncMaster => self.nudge_master_count(1),
|
||||||
DesktopAction::DecMaster => self.nudge_master_count(-1),
|
DesktopAction::DecMaster => self.nudge_master_count(-1),
|
||||||
DesktopAction::PromoteToMaster => {
|
DesktopAction::PromoteToMaster => {
|
||||||
self.workspaces[self.active].promote_focused();
|
self.workspaces[active].promote_focused();
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
DesktopAction::SwitchWorkspace(n) => {
|
DesktopAction::SwitchWorkspace(n) => {
|
||||||
if n < self.workspaces.len() && n != self.active {
|
if n < self.workspaces.len() && n != active {
|
||||||
self.active = n;
|
self.show_workspace(n);
|
||||||
self.relayout()
|
self.relayout()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DesktopAction::SendToWorkspace(n) => {
|
DesktopAction::SendToWorkspace(n) => {
|
||||||
if n >= self.workspaces.len() || n == self.active {
|
if n >= self.workspaces.len() || n == active {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
match self.workspaces[self.active].focused() {
|
match self.workspaces[active].focused() {
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
self.workspaces[self.active].remove(id);
|
self.workspaces[active].remove(id);
|
||||||
self.workspaces[n].add(id);
|
self.workspaces[n].add(id);
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DesktopAction::FocusOutputNext => {
|
||||||
|
if self.outputs.len() > 1 {
|
||||||
|
self.focused_output = (self.focused_output + 1) % self.outputs.len();
|
||||||
|
self.relayout()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
DesktopAction::Quit => vec![BrainCommand::Shutdown],
|
DesktopAction::Quit => vec![BrainCommand::Shutdown],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// El índice del escritorio activo — el que muestra la salida
|
||||||
|
/// enfocada. `0` si todavía no hay ninguna salida.
|
||||||
|
pub fn active_index(&self) -> usize {
|
||||||
|
self.outputs
|
||||||
|
.get(self.focused_output)
|
||||||
|
.map(|o| o.workspace)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hace que la salida enfocada muestre el escritorio `n`. Si otra
|
||||||
|
/// salida ya lo mostraba, intercambian — así ningún escritorio se
|
||||||
|
/// ve en dos sitios a la vez.
|
||||||
|
fn show_workspace(&mut self, n: usize) {
|
||||||
|
if n >= self.workspaces.len() || self.focused_output >= self.outputs.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current = self.outputs[self.focused_output].workspace;
|
||||||
|
if current == n {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(other) = self.outputs.iter().position(|o| o.workspace == n) {
|
||||||
|
self.outputs[other].workspace = current;
|
||||||
|
}
|
||||||
|
self.outputs[self.focused_output].workspace = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recoloca las salidas en fila horizontal, en su orden de aparición.
|
||||||
|
fn reflow_outputs(&mut self) {
|
||||||
|
let mut x = 0;
|
||||||
|
for o in &mut self.outputs {
|
||||||
|
o.rect.x = x;
|
||||||
|
o.rect.y = 0;
|
||||||
|
x += o.rect.w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Ajusta la fracción del área maestra del escritorio activo (la usan
|
/// Ajusta la fracción del área maestra del escritorio activo (la usan
|
||||||
/// `MasterStack` y `CenteredMaster`), acotada a `0.05..=0.95`.
|
/// `MasterStack` y `CenteredMaster`), acotada a `0.05..=0.95`.
|
||||||
fn nudge_master(&mut self, delta: f32) -> Vec<BrainCommand> {
|
fn nudge_master(&mut self, delta: f32) -> Vec<BrainCommand> {
|
||||||
let ws = &mut self.workspaces[self.active];
|
let active = self.active_index();
|
||||||
|
let ws = &mut self.workspaces[active];
|
||||||
let ratio = (ws.params().master_ratio + delta).clamp(0.05, 0.95);
|
let ratio = (ws.params().master_ratio + delta).clamp(0.05, 0.95);
|
||||||
ws.set_master_ratio(ratio);
|
ws.set_master_ratio(ratio);
|
||||||
self.relayout()
|
self.relayout()
|
||||||
@@ -283,52 +355,61 @@ impl Desktop {
|
|||||||
|
|
||||||
/// Ajusta `nmaster` del escritorio activo, acotado a `1..=9`.
|
/// Ajusta `nmaster` del escritorio activo, acotado a `1..=9`.
|
||||||
fn nudge_master_count(&mut self, delta: i32) -> Vec<BrainCommand> {
|
fn nudge_master_count(&mut self, delta: i32) -> Vec<BrainCommand> {
|
||||||
let ws = &mut self.workspaces[self.active];
|
let active = self.active_index();
|
||||||
|
let ws = &mut self.workspaces[active];
|
||||||
let n = (ws.params().master_count as i32 + delta).clamp(1, 9) as usize;
|
let n = (ws.params().master_count as i32 + delta).clamp(1, 9) as usize;
|
||||||
ws.set_master_count(n);
|
ws.set_master_count(n);
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recalcula la geometría del escritorio activo y la empaqueta en un
|
/// Recalcula la geometría de **todas** las salidas y la empaqueta en
|
||||||
/// [`BrainCommand::Place`]. Sin salida conectada, no hay nada que
|
/// un único [`BrainCommand::Place`]. Sin salidas, no hay nada que
|
||||||
/// colocar.
|
/// colocar.
|
||||||
fn relayout(&self) -> Vec<BrainCommand> {
|
fn relayout(&self) -> Vec<BrainCommand> {
|
||||||
match self.screen() {
|
if self.outputs.is_empty() {
|
||||||
Some(screen) => {
|
return Vec::new();
|
||||||
vec![BrainCommand::Place(placements(
|
|
||||||
&self.workspaces[self.active],
|
|
||||||
screen,
|
|
||||||
))]
|
|
||||||
}
|
}
|
||||||
None => Vec::new(),
|
let mut all = Vec::new();
|
||||||
|
for o in &self.outputs {
|
||||||
|
all.extend(placements(&self.workspaces[o.workspace], o.rect));
|
||||||
}
|
}
|
||||||
|
// El foco del teclado es único: sólo la ventana enfocada de la
|
||||||
|
// salida enfocada. `placements` marca el foco por escritorio (lo
|
||||||
|
// necesita para la visibilidad en `Monocle`); aquí lo unificamos.
|
||||||
|
let global_focus = self.focused_window();
|
||||||
|
for p in &mut all {
|
||||||
|
p.focused = Some(p.id) == global_focus;
|
||||||
|
}
|
||||||
|
vec![BrainCommand::Place(all)]
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Accesores de sólo lectura, para el HUD de la app GPUI ---------
|
// --- Accesores de sólo lectura, para el HUD de la app GPUI ---------
|
||||||
|
|
||||||
/// Índice del escritorio activo.
|
/// El escritorio activo — el de la salida enfocada.
|
||||||
pub fn active_index(&self) -> usize {
|
|
||||||
self.active
|
|
||||||
}
|
|
||||||
|
|
||||||
/// El escritorio activo.
|
|
||||||
pub fn active_workspace(&self) -> &Workspace {
|
pub fn active_workspace(&self) -> &Workspace {
|
||||||
&self.workspaces[self.active]
|
&self.workspaces[self.active_index()]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Las salidas conectadas, en orden.
|
/// Las salidas conectadas, en orden, con el escritorio que muestran.
|
||||||
pub fn outputs(&self) -> &[(OutputId, Rect)] {
|
pub fn outputs(&self) -> &[Output] {
|
||||||
&self.outputs
|
&self.outputs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Índice (en [`outputs`](Desktop::outputs)) de la salida enfocada.
|
||||||
|
pub fn focused_output(&self) -> usize {
|
||||||
|
self.focused_output
|
||||||
|
}
|
||||||
|
|
||||||
/// Identidad de una ventana conocida.
|
/// Identidad de una ventana conocida.
|
||||||
pub fn window_info(&self, id: WindowId) -> Option<&WindowInfo> {
|
pub fn window_info(&self, id: WindowId) -> Option<&WindowInfo> {
|
||||||
self.windows.get(&id)
|
self.windows.get(&id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// La ventana enfocada en el escritorio activo.
|
/// La ventana con el foco del teclado: la enfocada del escritorio
|
||||||
|
/// activo — o su ventana en pantalla completa, si la hay.
|
||||||
pub fn focused_window(&self) -> Option<WindowId> {
|
pub fn focused_window(&self) -> Option<WindowId> {
|
||||||
self.workspaces[self.active].focused()
|
let ws = &self.workspaces[self.active_index()];
|
||||||
|
ws.fullscreen().or_else(|| ws.focused())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cuántas ventanas hay en cada escritorio virtual.
|
/// Cuántas ventanas hay en cada escritorio virtual.
|
||||||
@@ -339,6 +420,7 @@ impl Desktop {
|
|||||||
/// Una vista de todas las ventanas conocidas, en todos los
|
/// Una vista de todas las ventanas conocidas, en todos los
|
||||||
/// escritorios — la base de `mirada-ctl windows` y de una taskbar.
|
/// escritorios — la base de `mirada-ctl windows` y de una taskbar.
|
||||||
pub fn window_lines(&self) -> Vec<crate::ctl::WindowLine> {
|
pub fn window_lines(&self) -> Vec<crate::ctl::WindowLine> {
|
||||||
|
let active = self.active_index();
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
for (n, ws) in self.workspaces.iter().enumerate() {
|
for (n, ws) in self.workspaces.iter().enumerate() {
|
||||||
let ws_focus = ws.focused();
|
let ws_focus = ws.focused();
|
||||||
@@ -349,7 +431,7 @@ impl Desktop {
|
|||||||
app_id: info.map(|i| i.app_id.clone()).unwrap_or_default(),
|
app_id: info.map(|i| i.app_id.clone()).unwrap_or_default(),
|
||||||
title: info.map(|i| i.title.clone()).unwrap_or_default(),
|
title: info.map(|i| i.title.clone()).unwrap_or_default(),
|
||||||
workspace: n + 1,
|
workspace: n + 1,
|
||||||
focused: n == self.active && ws_focus == Some(id),
|
focused: n == active && ws_focus == Some(id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -709,6 +791,16 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Multi-monitor -------------------------------------------------
|
||||||
|
|
||||||
|
/// Un escritorio con dos salidas 1920×1080.
|
||||||
|
fn desktop_with_two_outputs() -> Desktop {
|
||||||
|
let mut d = Desktop::new();
|
||||||
|
d.on_event(BodyEvent::OutputAdded { id: 0, width: 1920, height: 1080 });
|
||||||
|
d.on_event(BodyEvent::OutputAdded { id: 1, width: 1920, height: 1080 });
|
||||||
|
d
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn outputs_lay_side_by_side() {
|
fn outputs_lay_side_by_side() {
|
||||||
let mut d = Desktop::new();
|
let mut d = Desktop::new();
|
||||||
@@ -716,8 +808,69 @@ mod tests {
|
|||||||
d.on_event(BodyEvent::OutputAdded { id: 1, width: 2560, height: 1440 });
|
d.on_event(BodyEvent::OutputAdded { id: 1, width: 2560, height: 1440 });
|
||||||
assert_eq!(d.outputs().len(), 2);
|
assert_eq!(d.outputs().len(), 2);
|
||||||
// La segunda salida arranca donde acaba la primera.
|
// La segunda salida arranca donde acaba la primera.
|
||||||
assert_eq!(d.outputs()[1].1.x, 1920);
|
assert_eq!(d.outputs()[1].rect.x, 1920);
|
||||||
// El teselado sigue sobre la salida primaria.
|
}
|
||||||
assert_eq!(d.screen().unwrap().w, 1920);
|
|
||||||
|
#[test]
|
||||||
|
fn each_output_shows_a_distinct_workspace() {
|
||||||
|
let d = desktop_with_two_outputs();
|
||||||
|
assert_eq!(d.outputs()[0].workspace, 0);
|
||||||
|
assert_eq!(d.outputs()[1].workspace, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn switching_to_a_workspace_shown_on_another_output_swaps_them() {
|
||||||
|
let mut d = desktop_with_two_outputs();
|
||||||
|
// La salida enfocada (0, ws 0) pide el ws 1, que muestra la 1 → swap.
|
||||||
|
d.apply(DesktopAction::SwitchWorkspace(1));
|
||||||
|
assert_eq!(d.outputs()[0].workspace, 1);
|
||||||
|
assert_eq!(d.outputs()[1].workspace, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focus_output_next_moves_the_focus_between_outputs() {
|
||||||
|
let mut d = desktop_with_two_outputs();
|
||||||
|
assert_eq!(d.active_index(), 0); // salida 0 → ws 0
|
||||||
|
d.apply(DesktopAction::FocusOutputNext);
|
||||||
|
assert_eq!(d.active_index(), 1); // salida 1 → ws 1
|
||||||
|
d.apply(DesktopAction::FocusOutputNext); // envuelve
|
||||||
|
assert_eq!(d.active_index(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relayout_places_windows_on_every_output() {
|
||||||
|
let mut d = Desktop::new();
|
||||||
|
d.on_event(BodyEvent::OutputAdded { id: 0, width: 1920, height: 1080 });
|
||||||
|
d.on_event(BodyEvent::OutputAdded { id: 1, width: 1280, height: 720 });
|
||||||
|
open(&mut d, 1); // en la salida 0 (ws 0)
|
||||||
|
d.apply(DesktopAction::FocusOutputNext);
|
||||||
|
let cmds = open(&mut d, 2); // en la salida 1 (ws 1)
|
||||||
|
let p = places(&cmds);
|
||||||
|
assert_eq!(p.len(), 2);
|
||||||
|
// Cada ventana cae en el rectángulo de su salida.
|
||||||
|
assert_eq!(p.iter().find(|x| x.id == 1).unwrap().rect.x, 0);
|
||||||
|
assert_eq!(p.iter().find(|x| x.id == 2).unwrap().rect.x, 1920);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keyboard_focus_is_unique_across_outputs() {
|
||||||
|
let mut d = desktop_with_two_outputs();
|
||||||
|
open(&mut d, 1);
|
||||||
|
d.apply(DesktopAction::FocusOutputNext);
|
||||||
|
let cmds = open(&mut d, 2);
|
||||||
|
// Sólo una ventana con foco de teclado en todo el Place.
|
||||||
|
assert_eq!(places(&cmds).iter().filter(|p| p.focused).count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn removing_an_output_keeps_its_windows_in_their_workspace() {
|
||||||
|
let mut d = desktop_with_two_outputs();
|
||||||
|
d.apply(DesktopAction::FocusOutputNext); // foco en la salida 1 (ws 1)
|
||||||
|
open(&mut d, 1); // en ws 1
|
||||||
|
d.on_event(BodyEvent::OutputRemoved { id: 1 });
|
||||||
|
// La ventana sigue registrada, en el ws 1.
|
||||||
|
assert!(d.window_info(1).is_some());
|
||||||
|
assert_eq!(d.workspace_loads()[1], 1);
|
||||||
|
assert_eq!(d.outputs().len(), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ const KEYMAP_HEADER: &str = "\
|
|||||||
// grow-master / shrink-master redimensiona el área maestra
|
// grow-master / shrink-master redimensiona el área maestra
|
||||||
// inc-master / dec-master nº de ventanas maestras (nmaster)
|
// inc-master / dec-master nº de ventanas maestras (nmaster)
|
||||||
// promote-to-master la enfocada al puesto maestro
|
// promote-to-master la enfocada al puesto maestro
|
||||||
|
// focus-output-next pasa el foco al siguiente monitor
|
||||||
// workspace:N activa el escritorio N (1..9)
|
// workspace:N activa el escritorio N (1..9)
|
||||||
// send-to-workspace:N manda la enfocada al escritorio N
|
// send-to-workspace:N manda la enfocada al escritorio N
|
||||||
// quit apaga el compositor
|
// quit apaga el compositor
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ pub mod rules;
|
|||||||
|
|
||||||
pub use action::{default_keymap, DesktopAction, WORKSPACE_COUNT};
|
pub use action::{default_keymap, DesktopAction, WORKSPACE_COUNT};
|
||||||
pub use ctl::{CtlConn, CtlReply, CtlRequest, CtlServer, WindowLine};
|
pub use ctl::{CtlConn, CtlReply, CtlRequest, CtlServer, WindowLine};
|
||||||
pub use desktop::{Desktop, WindowInfo};
|
pub use desktop::{Desktop, Output, WindowInfo};
|
||||||
pub use keymap::{Keymap, KeymapError, KeymapWatch};
|
pub use keymap::{Keymap, KeymapError, KeymapWatch};
|
||||||
pub use rules::{Rule, RuleOutcome, Rules};
|
pub use rules::{Rule, RuleOutcome, Rules};
|
||||||
|
|
||||||
|
|||||||
@@ -1073,5 +1073,14 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Multi-monitor real — el Desktop tesela todas las salidas:
|
||||||
|
Cada Output muestra un escritorio distinto; relayout() las tesela todas en un solo Place.
|
||||||
|
Pedir un escritorio que ya muestra otra salida → las intercambia (ningún escritorio se ve 2 veces).
|
||||||
|
mirada-ctl focus-output-next # Super+o — mueve el foco al siguiente monitor
|
||||||
|
El foco del teclado es único: sólo la ventana enfocada de la salida enfocada.
|
||||||
|
cargo run -p mirada-brain --example headless-ctl # ahora levanta 2 salidas
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user