aboutsummaryrefslogtreecommitdiff
path: root/src/layout.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/layout.rs')
-rw-r--r--src/layout.rs1099
1 files changed, 1099 insertions, 0 deletions
diff --git a/src/layout.rs b/src/layout.rs
new file mode 100644
index 00000000..62a0423d
--- /dev/null
+++ b/src/layout.rs
@@ -0,0 +1,1099 @@
+//! 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.push(Monitor {
+ output,
+ workspaces,
+ active_workspace_idx: 0,
+ });
+ MonitorSet::Normal {
+ monitors,
+ primary_idx,
+ active_monitor_idx,
+ }
+ }
+ MonitorSet::NoOutputs(mut workspaces) => {
+ if workspaces.iter().all(|ws| ws.original_output != id) {
+ workspaces.insert(
+ 0,
+ Workspace {
+ original_output: id.clone(),
+ layout: Layout::new(),
+ space: Space::default(),
+ },
+ );
+ }
+
+ for workspace in &mut workspaces {
+ workspace.space.map_output(&output, (0, 0));
+ }
+
+ let monitor = Monitor {
+ output,
+ workspaces,
+ active_workspace_idx: 0,
+ };
+ MonitorSet::Normal {
+ monitors: vec![monitor],
+ primary_idx: 0,
+ active_monitor_idx: 0,
+ }
+ }
+ }
+ }
+
+ pub fn remove_output(&mut self, output: &Output) {
+ *self = match mem::take(self) {
+ MonitorSet::Normal {
+ mut monitors,
+ mut primary_idx,
+ mut active_monitor_idx,
+ } => {
+ let idx = monitors
+ .iter()
+ .position(|mon| &mon.output == output)
+ .expect("trying to remove non-existing output");
+ let monitor = monitors.remove(idx);
+ let mut workspaces = monitor.workspaces;
+
+ for ws in &mut workspaces {
+ ws.space.unmap_output(output);
+ }
+
+ // Get rid of empty workspaces.
+ workspaces.retain(|ws| ws.space.elements().next().is_some());
+
+ if monitors.is_empty() {
+ // Removed the last monitor.
+ MonitorSet::NoOutputs(workspaces)
+ } else {
+ if primary_idx >= idx {
+ // Update primary_idx to either still point at the same monitor, or at some
+ // other monitor if the primary has been removed.
+ primary_idx = primary_idx.saturating_sub(1);
+ }
+ if active_monitor_idx >= idx {
+ // Update active_monitor_idx to either still point at the same monitor, or
+ // at some other monitor if the active monitor has
+ // been removed.
+ active_monitor_idx = active_monitor_idx.saturating_sub(1);
+ }
+
+ let primary = &mut monitors[primary_idx];
+ for ws in &mut workspaces {
+ ws.space.map_output(&primary.output, (0, 0));
+ }
+ primary.workspaces.extend(workspaces);
+
+ MonitorSet::Normal {
+ monitors,
+ primary_idx,
+ active_monitor_idx,
+ }
+ }
+ }
+ MonitorSet::NoOutputs(_) => {
+ panic!("tried to remove output when there were already none")
+ }
+ }
+ }
+
+ pub fn configure_new_window(output: &Output, window: &Window) {
+ let output_size = output_size(output);
+ let size = Size::from((
+ (output_size.w - PADDING * 3) / 2,
+ output_size.h - PADDING * 2,
+ ));
+ let bounds = Size::from((output_size.w - PADDING * 2, output_size.h - PADDING * 2));
+
+ window.toplevel().with_pending_state(|state| {
+ state.size = Some(size);
+ state.bounds = Some(bounds);
+ });
+ }
+
+ pub fn add_window(
+ &mut self,
+ monitor_idx: usize,
+ workspace_idx: usize,
+ window: Window,
+ activate: bool,
+ ) {
+ let MonitorSet::Normal {
+ monitors,
+ active_monitor_idx,
+ ..
+ } = self
+ else {
+ panic!()
+ };
+
+ let monitor = &mut monitors[monitor_idx];
+ let workspace = &mut monitor.workspaces[workspace_idx];
+ workspace.layout.add_window(window.clone(), activate);
+ workspace.space.map_element(window.clone(), (0, 0), false);
+ workspace.layout.sync_space(&mut workspace.space);
+
+ MonitorSet::configure_new_window(&monitor.output, &window);
+ window.toplevel().send_pending_configure();
+
+ if activate {
+ *active_monitor_idx = monitor_idx;
+ monitor.active_workspace_idx = workspace_idx;
+ }
+
+ if workspace_idx == monitor.workspaces.len() - 1 {
+ // Insert a new empty workspace.
+ let mut ws = Workspace {
+ original_output: OutputId::new(&monitor.output),
+ layout: Layout::new(),
+ space: Space::default(),
+ };
+ ws.space.map_output(&monitor.output, (0, 0));
+ monitor.workspaces.push(ws);
+ }
+ }
+
+ pub fn add_window_to_output(&mut self, output: &Output, window: Window, activate: bool) {
+ let MonitorSet::Normal { monitors, .. } = self else {
+ panic!()
+ };
+
+ let (monitor_idx, monitor) = monitors
+ .iter()
+ .enumerate()
+ .find(|(_, mon)| &mon.output == output)
+ .unwrap();
+ let workspace_idx = monitor.active_workspace_idx;
+
+ self.add_window(monitor_idx, workspace_idx, window, activate)
+ }
+
+ pub fn remove_window(&mut self, window: &Window) {
+ let MonitorSet::Normal { monitors, .. } = self else {
+ panic!()
+ };
+
+ let (output, workspace) = monitors
+ .iter_mut()
+ .flat_map(|mon| mon.workspaces.iter_mut().map(|ws| (&mon.output, ws)))
+ .find(|(_, ws)| ws.space.elements().any(|win| win == window))
+ .unwrap();
+
+ workspace
+ .layout
+ .remove_window(window, output_size(output).h);
+ workspace.space.unmap_elem(window);
+ workspace.layout.sync_space(&mut workspace.space);
+
+ // FIXME: remove empty unfocused workspaces.
+ }
+
+ pub fn update_window(&mut self, window: &Window) {
+ let workspace = self
+ .workspaces()
+ .find(|ws| ws.space.elements().any(|w| w == window))
+ .unwrap();
+ workspace.layout.sync_space(&mut workspace.space);
+ }
+
+ pub fn activate_window(&mut self, window: &Window) {
+ let MonitorSet::Normal {
+ monitors,
+ active_monitor_idx,
+ ..
+ } = self
+ else {
+ todo!()
+ };
+
+ for (monitor_idx, mon) in monitors.iter_mut().enumerate() {
+ for (workspace_idx, ws) in mon.workspaces.iter_mut().enumerate() {
+ if ws.space.elements().any(|win| win == window) {
+ *active_monitor_idx = monitor_idx;
+ mon.active_workspace_idx = workspace_idx;
+
+ let changed = ws.layout.activate_window(window);
+ if changed {
+ ws.layout.sync_space(&mut ws.space);
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ pub fn activate_output(&mut self, output: &Output) {
+ let MonitorSet::Normal {
+ monitors,
+ active_monitor_idx,
+ ..
+ } = self
+ else {
+ return;
+ };
+
+ let idx = monitors
+ .iter()
+ .position(|mon| &mon.output == output)
+ .unwrap();
+ *active_monitor_idx = idx;
+ }
+
+ pub fn active_output(&self) -> Option<&Output> {
+ let MonitorSet::Normal {
+ monitors,
+ active_monitor_idx,
+ ..
+ } = self
+ else {
+ return None;
+ };
+
+ Some(&monitors[*active_monitor_idx].output)
+ }
+
+ fn active_workspace(&mut self) -> Option<&mut Workspace> {
+ let monitor = self.active_monitor()?;
+ Some(&mut monitor.workspaces[monitor.active_workspace_idx])
+ }
+
+ fn active_monitor(&mut self) -> Option<&mut Monitor> {
+ let MonitorSet::Normal {
+ monitors,
+ active_monitor_idx,
+ ..
+ } = self
+ else {
+ return None;
+ };
+
+ Some(&mut monitors[*active_monitor_idx])
+ }
+
+ pub fn move_left(&mut self) {
+ let Some(workspace) = self.active_workspace() else {
+ return;
+ };
+ let changed = workspace.layout.move_left();
+ if changed {
+ workspace.layout.sync_space(&mut workspace.space);
+ }
+ }
+
+ pub fn move_right(&mut self) {
+ let Some(workspace) = self.active_workspace() else {
+ return;
+ };
+ let changed = workspace.layout.move_right();
+ if changed {
+ workspace.layout.sync_space(&mut workspace.space);
+ }
+ }
+
+ pub fn move_down(&mut self) {
+ let Some(workspace) = self.active_workspace() else {
+ return;
+ };
+ let changed = workspace.layout.move_down();
+ if changed {
+ workspace.layout.sync_space(&mut workspace.space);
+ }
+ }
+
+ pub fn move_up(&mut self) {
+ let Some(workspace) = self.active_workspace() else {
+ return;
+ };
+ let changed = workspace.layout.move_up();
+ if changed {
+ workspace.layout.sync_space(&mut workspace.space);
+ }
+ }
+
+ pub fn focus_left(&mut self) {
+ let Some(workspace) = self.active_workspace() else {
+ return;
+ };
+ workspace.layout.focus_left();
+ }
+
+ pub fn focus_right(&mut self) {
+ let Some(workspace) = self.active_workspace() else {
+ return;
+ };
+ workspace.layout.focus_right();
+ }
+
+ pub fn focus_down(&mut self) {
+ let Some(workspace) = self.active_workspace() else {
+ return;
+ };
+ workspace.layout.focus_down();
+ }
+
+ pub fn focus_up(&mut self) {
+ let Some(workspace) = self.active_workspace() else {
+ return;
+ };
+ workspace.layout.focus_up();
+ }
+
+ pub fn move_to_workspace_up(&mut self) {
+ let MonitorSet::Normal {
+ monitors,
+ ref active_monitor_idx,
+ ..
+ } = self
+ else {
+ return;
+ };
+
+ let monitor = &mut monitors[*active_monitor_idx];
+
+ let new_idx = monitor.active_workspace_idx.saturating_sub(1);
+ if new_idx == monitor.active_workspace_idx {
+ return;
+ }
+
+ let workspace = &mut monitor.workspaces[monitor.active_workspace_idx];
+ if workspace.layout.columns.is_empty() {
+ return;
+ }
+
+ let column = &mut workspace.layout.columns[workspace.layout.active_column_idx];
+ let window = column.windows[column.active_window_idx].clone();
+ workspace
+ .layout
+ .remove_window(&window, output_size(&monitor.output).h);
+ workspace.space.unmap_elem(&window);
+ workspace.layout.sync_space(&mut workspace.space);
+
+ self.add_window(*active_monitor_idx, new_idx, window, true);
+
+ // FIXME: remove empty unfocused workspaces.
+ }
+
+ pub fn move_to_workspace_down(&mut self) {
+ let MonitorSet::Normal {
+ monitors,
+ ref active_monitor_idx,
+ ..
+ } = self
+ else {
+ return;
+ };
+
+ let monitor = &mut monitors[*active_monitor_idx];
+
+ let new_idx = min(
+ monitor.active_workspace_idx + 1,
+ monitor.workspaces.len() - 1,
+ );
+
+ if new_idx == monitor.active_workspace_idx {
+ return;
+ }
+
+ let workspace = &mut monitor.workspaces[monitor.active_workspace_idx];
+ if workspace.layout.columns.is_empty() {
+ return;
+ }
+
+ let column = &mut workspace.layout.columns[workspace.layout.active_column_idx];
+ let window = column.windows[column.active_window_idx].clone();
+ workspace
+ .layout
+ .remove_window(&window, output_size(&monitor.output).h);
+ workspace.space.unmap_elem(&window);
+ workspace.layout.sync_space(&mut workspace.space);
+
+ self.add_window(*active_monitor_idx, new_idx, window, true);
+
+ // FIXME: remove empty unfocused workspaces.
+ }
+
+ pub fn switch_workspace_up(&mut self) {
+ let Some(monitor) = self.active_monitor() else {
+ return;
+ };
+
+ monitor.active_workspace_idx = monitor.active_workspace_idx.saturating_sub(1);
+
+ // FIXME: remove empty unfocused workspaces.
+ }
+
+ pub fn switch_workspace_down(&mut self) {
+ let Some(monitor) = self.active_monitor() else {
+ return;
+ };
+ monitor.active_workspace_idx = min(
+ monitor.active_workspace_idx + 1,
+ monitor.workspaces.len() - 1,
+ );
+
+ // FIXME: remove empty unfocused workspaces.
+ }
+
+ pub fn consume_into_column(&mut self) {
+ let MonitorSet::Normal {
+ monitors,
+ active_monitor_idx,
+ ..
+ } = self
+ else {
+ return;
+ };
+
+ let monitor = &mut monitors[*active_monitor_idx];
+
+ let workspace = &mut monitor.workspaces[monitor.active_workspace_idx];
+ let changed = workspace
+ .layout
+ .consume_into_column(output_size(&monitor.output).h);
+ if changed {
+ workspace.layout.sync_space(&mut workspace.space);
+ }
+ }
+
+ pub fn expel_from_column(&mut self) {
+ let MonitorSet::Normal {
+ monitors,
+ active_monitor_idx,
+ ..
+ } = self
+ else {
+ return;
+ };
+
+ let monitor = &mut monitors[*active_monitor_idx];
+
+ let output_scale = monitor.output.current_scale().integer_scale();
+ let output_transform = monitor.output.current_transform();
+ let output_mode = monitor.output.current_mode().unwrap();
+ let output_size = output_transform
+ .transform_size(output_mode.size)
+ .to_logical(output_scale);
+
+ let workspace = &mut monitor.workspaces[monitor.active_workspace_idx];
+ let changed = workspace.layout.expel_from_column(output_size.h);
+ if changed {
+ workspace.layout.sync_space(&mut workspace.space);
+ }
+ }
+
+ pub fn focus(&self) -> Option<&Window> {
+ let MonitorSet::Normal {
+ monitors,
+ active_monitor_idx,
+ ..
+ } = self
+ else {
+ return None;
+ };
+
+ let monitor = &monitors[*active_monitor_idx];
+ let workspace = &monitor.workspaces[monitor.active_workspace_idx];
+ if workspace.layout.columns.is_empty() {
+ return None;
+ }
+
+ let column = &workspace.layout.columns[workspace.layout.active_column_idx];
+ Some(&column.windows[column.active_window_idx])
+ }
+
+ pub fn workspace_for_output(&mut self, output: &Output) -> Option<&mut Workspace> {
+ let MonitorSet::Normal { monitors, .. } = self else {
+ return None;
+ };
+
+ monitors.iter_mut().find_map(|monitor| {
+ if &monitor.output == output {
+ Some(&mut monitor.workspaces[monitor.active_workspace_idx])
+ } else {
+ None
+ }
+ })
+ }
+
+ pub fn workspaces(&mut self) -> impl Iterator<Item = &mut Workspace> + '_ {
+ match self {
+ MonitorSet::Normal { monitors, .. } => {
+ monitors.iter_mut().flat_map(|mon| &mut mon.workspaces)
+ }
+ MonitorSet::NoOutputs(_workspaces) => todo!(),
+ }
+ }
+
+ pub fn spaces(&mut self) -> impl Iterator<Item = &Space<Window>> + '_ {
+ self.workspaces().map(|workspace| &workspace.space)
+ }
+
+ pub fn find_window(&mut self, wl_surface: &WlSurface) -> Option<&Window> {
+ self.workspaces()
+ .flat_map(|workspace| workspace.space.elements())
+ .find(|window| window.toplevel().wl_surface() == wl_surface)
+ }
+
+ pub fn find_window_and_space(
+ &mut self,
+ wl_surface: &WlSurface,
+ ) -> Option<(Window, &Space<Window>)> {
+ self.spaces().find_map(|space| {
+ let window = space
+ .elements()
+ .find(|window| window.toplevel().wl_surface() == wl_surface)
+ .cloned();
+ window.map(|window| (window, space))
+ })
+ }
+
+ /// Refreshes the `Space`s.
+ pub fn refresh(&mut self) {
+ for workspace in self.workspaces() {
+ workspace.space.refresh();
+ }
+ }
+
+ fn verify_invariants(&self) {
+ let (monitors, &primary_idx, &active_monitor_idx) = match &self {
+ MonitorSet::Normal {
+ monitors,
+ primary_idx,
+ active_monitor_idx,
+ } => (monitors, primary_idx, active_monitor_idx),
+ MonitorSet::NoOutputs(workspaces) => {
+ for workspace in workspaces {
+ assert!(
+ !workspace.layout.has_windows(),
+ "with no outputs there cannot be empty workspaces"
+ );
+
+ workspace.layout.verify_invariants();
+ }
+
+ return;
+ }
+ };
+
+ assert!(primary_idx <= monitors.len());
+ assert!(active_monitor_idx <= monitors.len());
+
+ for (idx, monitor) in monitors.iter().enumerate() {
+ assert!(
+ !monitor.workspaces.is_empty(),
+ "monitor monitor must have at least one workspace"
+ );
+
+ let monitor_id = OutputId::new(&monitor.output);
+
+ if idx == primary_idx {
+ assert!(
+ monitor
+ .workspaces
+ .iter()
+ .any(|workspace| workspace.original_output == monitor_id),
+ "primary monitor must have at least one own workspace"
+ );
+ } else {
+ assert!(
+ monitor
+ .workspaces
+ .iter()
+ .any(|workspace| workspace.original_output == monitor_id),
+ "secondary monitor must have all own workspaces"
+ );
+ }
+
+ // FIXME: verify that primary doesn't have any workspaces for which their own monitor
+ // exists.
+
+ for workspace in &monitor.workspaces {
+ workspace.layout.verify_invariants();
+ }
+ }
+ }
+}
+
+fn output_size(output: &Output) -> Size<i32, Logical> {
+ let output_scale = output.current_scale().integer_scale();
+ let output_transform = output.current_transform();
+ let output_mode = output.current_mode().unwrap();
+ let output_size = output_transform
+ .transform_size(output_mode.size)
+ .to_logical(output_scale);
+ output_size
+}
+
+impl Default for MonitorSet {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Layout {
+ fn new() -> Self {
+ Self {
+ columns: vec![],
+ active_column_idx: 0,
+ }
+ }
+
+ fn sync_space(&self, space: &mut Space<Window>) {
+ // FIXME: this is really inefficient
+ let mut active_window = None;
+
+ let mut x = PADDING;
+ for (column_idx, column) in self.columns.iter().enumerate() {
+ let mut y = PADDING;
+ for (window_idx, window) in column.windows.iter().enumerate() {
+ let active =
+ column_idx == self.active_column_idx && window_idx == column.active_window_idx;
+ if active {
+ active_window = Some(window.clone());
+ }
+
+ window.set_activated(active);
+ space.map_element(window.clone(), (x, y), false);
+ window.toplevel().send_pending_configure();
+ y += window.geometry().size.h + PADDING;
+ }
+ x += column.size().w + PADDING;
+ }
+
+ if let Some(window) = active_window {
+ space.raise_element(&window, false);
+ }
+ }
+
+ fn has_windows(&self) -> bool {
+ self.columns.is_empty()
+ }
+
+ /// Computes the width of the layout including left and right padding, in Logical coordinates.
+ fn width(&self) -> i32 {
+ let mut total = PADDING;
+
+ for column in &self.columns {
+ total += column.size().w + PADDING;
+ }
+
+ total
+ }
+
+ /// Computes the X position of the windows in the given column, in logical coordinates.
+ fn column_x(&self, column_idx: usize) -> i32 {
+ let mut x = PADDING;
+
+ for column in self.columns.iter().take(column_idx) {
+ x += column.size().w + PADDING;
+ }
+
+ x
+ }
+
+ fn add_window(&mut self, window: Window, activate: bool) {
+ let idx = if self.columns.is_empty() {
+ 0
+ } else {
+ self.active_column_idx + 1
+ };
+
+ let column = Column {
+ windows: vec![window],
+ active_window_idx: 0,
+ };
+ self.columns.insert(idx, column);
+
+ if activate {
+ self.active_column_idx = idx;
+ }
+ }
+
+ fn remove_window(&mut self, window: &Window, total_height: i32) {
+ let column_idx = self
+ .columns
+ .iter()
+ .position(|col| col.windows.contains(window))
+ .unwrap();
+ let column = &mut self.columns[column_idx];
+
+ let window_idx = column.windows.iter().position(|win| win == window).unwrap();
+ column.windows.remove(window_idx);
+ if column.windows.is_empty() {
+ self.columns.remove(column_idx);
+ if self.columns.is_empty() {
+ return;
+ }
+
+ self.active_column_idx = min(self.active_column_idx, self.columns.len() - 1);
+ return;
+ }
+
+ column.active_window_idx = min(column.active_window_idx, column.windows.len() - 1);
+
+ // Update window sizes.
+ let window_count = column.windows.len() as i32;
+ let height = (total_height - PADDING * (window_count + 1)) / window_count;
+ let width = column.size().w;
+
+ for window in &mut column.windows {
+ window
+ .toplevel()
+ .with_pending_state(|state| state.size = Some(Size::from((width, height))));
+ window.toplevel().send_pending_configure();
+ }
+ }
+
+ fn activate_window(&mut self, window: &Window) -> bool {
+ let column_idx = self
+ .columns
+ .iter()
+ .position(|col| col.windows.contains(window))
+ .unwrap();
+ let column = &mut self.columns[column_idx];
+
+ let window_idx = column.windows.iter().position(|win| win == window).unwrap();
+
+ if column.active_window_idx != window_idx || self.active_column_idx != column_idx {
+ column.active_window_idx = window_idx;
+ self.active_column_idx = column_idx;
+ true
+ } else {
+ false
+ }
+ }
+
+ fn verify_invariants(&self) {
+ for column in &self.columns {
+ column.verify_invariants();
+ }
+ }
+
+ fn focus_left(&mut self) {
+ self.active_column_idx = self.active_column_idx.saturating_sub(1);
+ }
+
+ fn focus_right(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ self.active_column_idx = min(self.active_column_idx + 1, self.columns.len() - 1);
+ }
+
+ fn focus_down(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ self.columns[self.active_column_idx].focus_down();
+ }
+
+ fn focus_up(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ self.columns[self.active_column_idx].focus_up();
+ }
+
+ fn move_left(&mut self) -> bool {
+ let new_idx = self.active_column_idx.saturating_sub(1);
+ if self.active_column_idx == new_idx {
+ return false;
+ }
+
+ self.columns.swap(self.active_column_idx, new_idx);
+ self.active_column_idx = new_idx;
+ true
+ }
+
+ fn move_right(&mut self) -> bool {
+ if self.columns.is_empty() {
+ return false;
+ }
+
+ let new_idx = min(self.active_column_idx + 1, self.columns.len() - 1);
+ if self.active_column_idx == new_idx {
+ return false;
+ }
+
+ self.columns.swap(self.active_column_idx, new_idx);
+ self.active_column_idx = new_idx;
+ true
+ }
+
+ fn move_down(&mut self) -> bool {
+ if self.columns.is_empty() {
+ return false;
+ }
+
+ self.columns[self.active_column_idx].move_down()
+ }
+
+ fn move_up(&mut self) -> bool {
+ if self.columns.is_empty() {
+ return false;
+ }
+
+ self.columns[self.active_column_idx].move_up()
+ }
+
+ fn consume_into_column(&mut self, total_height: i32) -> bool {
+ if self.columns.len() < 2 {
+ return false;
+ }
+
+ if self.active_column_idx == self.columns.len() - 1 {
+ return false;
+ }
+
+ let source_column_idx = self.active_column_idx + 1;
+
+ let source_column = &mut self.columns[source_column_idx];
+ let window = source_column.windows[0].clone();
+ self.remove_window(&window, total_height);
+
+ let target_column = &mut self.columns[self.active_column_idx];
+
+ let window_count = target_column.windows.len() as i32 + 1;
+ let height = (total_height - PADDING * (window_count + 1)) / window_count;
+ let width = target_column.size().w;
+
+ target_column.windows.push(window);
+
+ for window in &mut target_column.windows {
+ window
+ .toplevel()
+ .with_pending_state(|state| state.size = Some(Size::from((width, height))));
+ window.toplevel().send_pending_configure();
+ }
+ true
+ }
+
+ fn expel_from_column(&mut self, total_height: i32) -> bool {
+ if self.columns.is_empty() {
+ return false;
+ }
+
+ let source_column = &mut self.columns[self.active_column_idx];
+ if source_column.windows.len() == 1 {
+ return false;
+ }
+
+ let window = source_column.windows[source_column.active_window_idx].clone();
+ self.remove_window(&window, total_height);
+
+ window.toplevel().with_pending_state(|state| {
+ state.size = Some(Size::from((state.size.unwrap().w, total_height)))
+ });
+ window.toplevel().send_pending_configure();
+ self.add_window(window, true);
+
+ true
+ }
+}
+
+impl Column {
+ /// Computes the size of the column including top and bottom padding.
+ fn size(&self) -> Size<i32, Logical> {
+ let mut total = Size::from((0, PADDING));
+
+ for window in &self.windows {
+ let size = window.geometry().size;
+ total.w = max(total.w, size.w);
+ total.h += size.h + PADDING;
+ }
+
+ total
+ }
+
+ fn window_y(&self, window_idx: usize) -> i32 {
+ let mut y = PADDING;
+
+ for window in self.windows.iter().take(window_idx) {
+ let size = window.geometry().size;
+ y += size.h + PADDING;
+ }
+
+ y
+ }
+
+ fn focus_up(&mut self) {
+ self.active_window_idx = self.active_window_idx.saturating_sub(1);
+ }
+
+ fn focus_down(&mut self) {
+ self.active_window_idx = min(self.active_window_idx + 1, self.windows.len() - 1);
+ }
+
+ fn move_up(&mut self) -> bool {
+ let new_idx = self.active_window_idx.saturating_sub(1);
+ if self.active_window_idx == new_idx {
+ return false;
+ }
+
+ self.windows.swap(self.active_window_idx, new_idx);
+ self.active_window_idx = new_idx;
+ true
+ }
+
+ fn move_down(&mut self) -> bool {
+ let new_idx = min(self.active_window_idx + 1, self.windows.len() - 1);
+ if self.active_window_idx == new_idx {
+ return false;
+ }
+
+ self.windows.swap(self.active_window_idx, new_idx);
+ self.active_window_idx = new_idx;
+ true
+ }
+
+ fn verify_invariants(&self) {
+ assert!(!self.windows.is_empty(), "columns can't be empty");
+ }
+}