//! Window layout logic.
//!
//! Niri implements scrollable tiling with workspaces. There's one primary output, and potentially
//! multiple other outputs.
//!
//! Our layout has the following invariants:
//!
//! 1. Disconnecting and reconnecting the same output must not change the layout.
//! * This includes both secondary outputs and the primary output.
//! 2. Connecting an output must not change the layout for any workspaces that were never on that
//! output.
//!
//! Therefore, we implement the following logic: every workspace keeps track of which output it
//! originated on. When an output disconnects, its workspace (or workspaces, in case of the primary
//! output disconnecting) are appended to the (potentially new) primary output, but remember their
//! original output. Then, if the original output connects again, all workspaces originally from
//! there move back to that output.
//!
//! In order to avoid surprising behavior, if the user creates or moves any new windows onto a
//! workspace, it forgets its original output, and its current output becomes its original output.
//! Imagine a scenario: the user works with a laptop and a monitor at home, then takes their laptop
//! with them, disconnecting the monitor, and keeps working as normal, using the second monitor's
//! workspace just like any other. Then they come back, reconnect the second monitor, and now we
//! don't want an unassuming workspace to end up on it.
//!
//! ## Workspaces-only-on-primary considerations
//!
//! If this logic results in more than one workspace present on a secondary output, then as a
//! compromise we only keep the first workspace there, and move the rest to the primary output,
//! making the primary output their original output.
use std::cmp::{max, min};
use std::mem;
use smithay::desktop::{Space, Window};
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Size};
const PADDING: i32 = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputId(String);
#[derive(Debug)]
pub enum MonitorSet {
/// At least one output is connected.
Normal {
monitors: Vec<Monitor>,
/// Index of the primary monitor.
primary_idx: usize,
/// Index of the active monitor.
active_monitor_idx: usize,
},
/// No outputs are connected, and these are the workspaces.
// FIXME: preserve active output id?
NoOutputs(Vec<Workspace>),
}
#[derive(Debug)]
pub struct Monitor {
output: Output,
// Must always contain at least one.
workspaces: Vec<Workspace>,
/// Index of the currently active workspace.
active_workspace_idx: usize,
}
#[derive(Debug)]
pub struct Workspace {
/// The original output of this workspace.
///
/// Most of the time this will be the workspace's current output, however, after an output
/// disconnection, it may remain pointing to the disconnected output.
original_output: OutputId,
layout: Layout,
// The actual Space with windows in this workspace. Should be synchronized to the layout except
// for a brief period during surface commit handling.
pub space: Space<Window>,
}
#[derive(Debug)]
pub struct Layout {
columns: Vec<Column>,
/// Index of the currently active column, if any.
active_column_idx: usize,
}
#[derive(Debug)]
pub struct Column {
// Must be non-empty.
windows: Vec<Window>,
/// Index of the currently active window.
active_window_idx: usize,
}
impl OutputId {
pub fn new(output: &Output) -> Self {
Self(output.name())
}
}
impl MonitorSet {
pub fn new() -> Self {
Self::NoOutputs(vec![])
}
pub fn add_output(&mut self, output: Output) {
let id = OutputId::new(&output);
*self = match mem::take(self) {
MonitorSet::Normal {
mut monitors,
primary_idx,
active_monitor_idx,
} => {
let primary = &mut monitors[primary_idx];
let mut workspaces = vec![];
for i in (0..primary.workspaces.len()).rev() {
if primary.workspaces[i].original_output == id {
let mut ws = primary.workspaces.remove(i);
ws.space.unmap_output(&primary.output);
workspaces.push(ws);
}
}
workspaces.reverse();
if workspaces
.iter()
.all(|ws| ws.space.elements().next().is_some())
{
// Make sure there's always an empty workspace.
workspaces.push(Workspace {
original_output: id,
layout: Layout::new(),
space: Space::default(),
});
}
for ws in &mut workspaces {
ws.space.map_output(&output, (0, 0));
}
monitors.