Files
niri/src/layout/mod.rs
T

6555 lines
221 KiB
Rust
Raw Normal View History

//! 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::min;
use std::collections::HashMap;
use std::mem;
use std::rc::Rc;
use std::time::Duration;
2024-09-05 23:37:10 +02:00
use niri_config::{
2024-11-02 09:42:46 +03:00
CenterFocusedColumn, Config, CornerRadius, FloatOrInt, PresetSize, Struts,
Workspace as WorkspaceConfig,
2024-09-05 23:37:10 +02:00
};
2024-02-10 09:33:32 +04:00
use niri_ipc::SizeChange;
2024-11-30 09:18:33 +03:00
use scrolling::{Column, ColumnWidth, InsertHint, InsertPosition};
2024-02-04 22:29:09 +04:00
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
2024-03-19 14:41:17 +04:00
use smithay::backend::renderer::element::Id;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
2024-05-29 13:32:11 +03:00
use smithay::output::{self, Output};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
2024-07-15 15:51:48 +02:00
use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size, Transform};
use tile::{Tile, TileRenderElement};
2024-06-20 12:04:10 +03:00
use workspace::WorkspaceId;
2023-12-24 15:10:09 +04:00
pub use self::monitor::MonitorRenderElement;
use self::monitor::{Monitor, WorkspaceSwitch};
2024-11-30 09:18:33 +03:00
use self::workspace::{OutputId, Workspace};
use crate::animation::Clock;
use crate::niri_render_elements;
2024-02-06 11:24:50 +04:00
use crate::render_helpers::renderer::NiriRenderer;
2024-04-13 14:16:07 +04:00
use crate::render_helpers::snapshot::RenderSnapshot;
2024-06-17 09:16:28 +03:00
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::texture::TextureBuffer;
use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements};
2024-07-15 15:51:48 +02:00
use crate::rubber_band::RubberBand;
2024-08-23 15:41:06 +03:00
use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::{output_matches_name, output_size, round_logical_in_physical_max1, ResizeEdge};
use crate::window::ResolvedWindowRules;
2023-08-14 15:54:11 +04:00
2024-04-09 22:37:10 +04:00
pub mod closing_window;
2024-11-29 21:11:02 +03:00
pub mod floating;
2024-02-05 17:27:09 +04:00
pub mod focus_ring;
2024-11-02 09:33:44 +03:00
pub mod insert_hint_element;
2024-02-05 17:27:09 +04:00
pub mod monitor;
2024-05-15 19:38:29 +04:00
pub mod opening_window;
2024-11-30 09:18:33 +03:00
pub mod scrolling;
2024-02-05 17:27:09 +04:00
pub mod tile;
pub mod workspace;
2023-08-14 16:19:43 +04:00
2024-04-13 11:07:23 +04:00
/// Size changes up to this many pixels don't animate.
2024-06-17 09:16:28 +03:00
pub const RESIZE_ANIMATION_THRESHOLD: f64 = 10.;
2024-04-13 11:07:23 +04:00
2024-07-15 15:51:48 +02:00
/// Pointer needs to move this far to pull a window from the layout.
const INTERACTIVE_MOVE_START_THRESHOLD: f64 = 256. * 256.;
niri_render_elements! {
LayoutElementRenderElement<R> => {
Wayland = WaylandSurfaceRenderElement<R>,
SolidColor = SolidColorRenderElement,
}
2024-02-04 22:29:09 +04:00
}
pub type LayoutElementRenderSnapshot =
RenderSnapshot<BakedBuffer<TextureBuffer<GlesTexture>>, BakedBuffer<SolidColorBuffer>>;
2024-04-09 22:37:10 +04:00
pub trait LayoutElement {
/// Type that can be used as a unique ID of this element.
2024-10-18 08:40:52 +03:00
type Id: PartialEq + std::fmt::Debug + Clone;
/// Unique ID of this element.
fn id(&self) -> &Self::Id;
/// Visual size of the element.
///
/// This is what the user would consider the size, i.e. excluding CSD shadows and whatnot.
/// Corresponds to the Wayland window geometry size.
fn size(&self) -> Size<i32, Logical>;
/// Returns the location of the element's buffer relative to the element's visual geometry.
///
/// I.e. if the element has CSD shadows, its buffer location will have negative coordinates.
fn buf_loc(&self) -> Point<i32, Logical>;
/// Checks whether a point is in the element's input region.
///
/// The point is relative to the element's visual geometry.
fn is_in_input_region(&self, point: Point<f64, Logical>) -> bool;
/// Renders the element at the given visual location.
///
/// The element should be rendered in such a way that its visual geometry ends up at the given
/// location.
fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
2024-06-17 09:16:28 +03:00
location: Point<f64, Logical>,
scale: Scale<f64>,
2024-03-24 08:30:26 +04:00
alpha: f32,
target: RenderTarget,
) -> SplitElements<LayoutElementRenderElement<R>>;
/// Renders the non-popup parts of the element.
fn render_normal<R: NiriRenderer>(
&self,
renderer: &mut R,
2024-06-17 09:16:28 +03:00
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
) -> Vec<LayoutElementRenderElement<R>> {
self.render(renderer, location, scale, alpha, target).normal
}
/// Renders the popups of the element.
fn render_popups<R: NiriRenderer>(
&self,
renderer: &mut R,
2024-06-17 09:16:28 +03:00
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
) -> Vec<LayoutElementRenderElement<R>> {
self.render(renderer, location, scale, alpha, target).popups
}
2024-08-22 14:44:11 +03:00
fn request_size(
&mut self,
size: Size<i32, Logical>,
animate: bool,
transaction: Option<Transaction>,
);
2023-08-16 09:08:10 +04:00
fn request_fullscreen(&self, size: Size<i32, Logical>);
fn min_size(&self) -> Size<i32, Logical>;
2023-08-16 10:03:24 +04:00
fn max_size(&self) -> Size<i32, Logical>;
fn is_wl_surface(&self, wl_surface: &WlSurface) -> bool;
2023-10-11 14:32:29 +04:00
fn has_ssd(&self) -> bool;
2024-05-29 13:32:11 +03:00
fn set_preferred_scale_transform(&self, scale: output::Scale, transform: Transform);
fn output_enter(&self, output: &Output);
fn output_leave(&self, output: &Output);
2024-02-07 11:32:02 +04:00
fn set_offscreen_element_id(&self, id: Option<Id>);
2024-03-23 14:38:07 +04:00
fn set_activated(&mut self, active: bool);
2024-04-22 22:51:52 +02:00
fn set_active_in_column(&mut self, active: bool);
2024-03-19 13:52:08 +04:00
fn set_bounds(&self, bounds: Size<i32, Logical>);
2024-08-22 14:36:47 +03:00
fn configure_intent(&self) -> ConfigureIntent;
2024-04-13 11:07:23 +04:00
fn send_pending_configure(&mut self);
/// Whether the element is currently fullscreen.
///
/// This will *not* switch immediately after a [`LayoutElement::request_fullscreen()`] call.
fn is_fullscreen(&self) -> bool;
/// Whether we're requesting the element to be fullscreen.
///
/// This *will* switch immediately after a [`LayoutElement::request_fullscreen()`] call.
fn is_pending_fullscreen(&self) -> bool;
2024-03-19 13:52:08 +04:00
/// Size previously requested through [`LayoutElement::request_size()`].
fn requested_size(&self) -> Option<Size<i32, Logical>>;
fn rules(&self) -> &ResolvedWindowRules;
2024-03-19 13:52:08 +04:00
/// Runs periodic clean-up tasks.
fn refresh(&self);
2024-04-13 11:07:23 +04:00
2024-04-13 14:16:07 +04:00
fn animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot>;
fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot>;
2024-05-10 16:58:53 +04:00
fn set_interactive_resize(&mut self, data: Option<InteractiveResizeData>);
fn cancel_interactive_resize(&mut self);
2024-05-11 08:26:49 +04:00
fn update_interactive_resize(&mut self, serial: Serial);
fn interactive_resize_data(&self) -> Option<InteractiveResizeData>;
}
#[derive(Debug)]
pub struct Layout<W: LayoutElement> {
/// Monitors and workspaes in the layout.
monitor_set: MonitorSet<W>,
/// Whether the layout should draw as active.
///
/// This normally indicates that the layout has keyboard focus, but not always. E.g. when the
/// screenshot UI is open, it keeps the layout drawing as active.
is_active: bool,
/// Map from monitor name to id of its last active workspace.
///
/// This data is stored upon monitor removal and is used to restore the active workspace when
/// the monitor is reconnected.
///
/// The workspace id does not necessarily point to a valid workspace. If it doesn't, then it is
/// simply ignored.
last_active_workspace_id: HashMap<String, WorkspaceId>,
2024-07-15 15:51:48 +02:00
/// Ongoing interactive move.
interactive_move: Option<InteractiveMoveState<W>>,
/// Clock for driving animations.
clock: Clock,
/// Time that we last updated render elements for.
update_render_elements_time: Duration,
/// Configurable properties of the layout.
options: Rc<Options>,
}
#[derive(Debug)]
enum MonitorSet<W: LayoutElement> {
/// At least one output is connected.
Normal {
/// Connected monitors.
monitors: Vec<Monitor<W>>,
/// 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.
NoOutputs {
/// The workspaces.
workspaces: Vec<Workspace<W>>,
},
}
2024-06-17 09:16:28 +03:00
#[derive(Debug, Clone, PartialEq)]
2023-12-24 15:10:09 +04:00
pub struct Options {
2023-10-07 17:45:55 +04:00
/// Padding around windows in logical pixels.
2024-06-17 09:16:28 +03:00
pub gaps: f64,
2023-12-21 08:37:30 +04:00
/// Extra padding around the working area in logical pixels.
2024-02-05 17:27:09 +04:00
pub struts: Struts,
pub focus_ring: niri_config::FocusRing,
pub border: niri_config::Border,
2024-07-15 15:51:48 +02:00
pub insert_hint: niri_config::InsertHint,
2024-02-05 17:27:09 +04:00
pub center_focused_column: CenterFocusedColumn,
pub always_center_single_column: bool,
pub empty_workspace_above_first: bool,
/// Column widths that `toggle_width()` switches between.
2024-09-05 23:37:10 +02:00
pub preset_column_widths: Vec<ColumnWidth>,
/// Initial width for new columns.
2024-09-05 23:37:10 +02:00
pub default_column_width: Option<ColumnWidth>,
/// Window height that `toggle_window_height()` switches between.
pub preset_window_heights: Vec<PresetSize>,
2024-02-07 17:05:15 +04:00
pub animations: niri_config::Animations,
2024-08-22 14:36:47 +03:00
// Debug flags.
pub disable_resize_throttling: bool,
2024-08-22 14:44:11 +03:00
pub disable_transactions: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
2024-06-17 09:16:28 +03:00
gaps: 16.,
2023-12-21 08:37:30 +04:00
struts: Default::default(),
focus_ring: Default::default(),
border: Default::default(),
2024-07-15 15:51:48 +02:00
insert_hint: Default::default(),
2024-01-08 17:17:19 +04:00
center_focused_column: Default::default(),
always_center_single_column: false,
empty_workspace_above_first: false,
2024-09-05 23:37:10 +02:00
preset_column_widths: vec![
ColumnWidth::Proportion(1. / 3.),
ColumnWidth::Proportion(0.5),
ColumnWidth::Proportion(2. / 3.),
],
2024-09-05 23:37:10 +02:00
default_column_width: None,
2024-02-07 17:05:15 +04:00
animations: Default::default(),
2024-08-22 14:36:47 +03:00
disable_resize_throttling: false,
2024-08-22 14:44:11 +03:00
disable_transactions: false,
2024-09-05 23:37:10 +02:00
preset_window_heights: vec![
PresetSize::Proportion(1. / 3.),
PresetSize::Proportion(0.5),
PresetSize::Proportion(2. / 3.),
],
}
}
}
2024-11-05 21:08:50 +03:00
#[derive(Debug)]
enum InteractiveMoveState<W: LayoutElement> {
/// Initial rubberbanding; the window remains in the layout.
Starting {
/// The window we're moving.
window_id: W::Id,
/// Current pointer delta from the starting location.
pointer_delta: Point<f64, Logical>,
/// Pointer location within the visual window geometry as ratio from geometry size.
///
/// This helps the pointer remain inside the window as it resizes.
pointer_ratio_within_window: (f64, f64),
},
/// Moving; the window is no longer in the layout.
Moving(InteractiveMoveData<W>),
}
#[derive(Debug)]
struct InteractiveMoveData<W: LayoutElement> {
/// The window being moved.
pub(self) tile: Tile<W>,
/// Output where the window is currently located/rendered.
pub(self) output: Output,
/// Current pointer position within output.
pub(self) pointer_pos_within_output: Point<f64, Logical>,
/// Window column width.
pub(self) width: ColumnWidth,
/// Whether the window column was full-width.
pub(self) is_full_width: bool,
/// Whether the window targets the floating layout.
pub(self) is_floating: bool,
2024-11-05 21:08:50 +03:00
/// Pointer location within the visual window geometry as ratio from geometry size.
///
/// This helps the pointer remain inside the window as it resizes.
pub(self) pointer_ratio_within_window: (f64, f64),
}
#[derive(Debug, Clone, Copy)]
pub struct InteractiveResizeData {
pub(self) edges: ResizeEdge,
}
#[derive(Debug, Clone, Copy)]
pub enum ConfigureIntent {
/// A configure is not needed (no changes to server pending state).
NotNeeded,
/// A configure is throttled (due to resizing too fast for example).
Throttled,
/// Can send the configure if it isn't throttled externally (only size changed).
CanSend,
/// Should send the configure regardless of external throttling (something other than size
/// changed).
ShouldSend,
}
2024-10-14 18:08:44 +03:00
/// Tile that was just removed from the layout.
pub struct RemovedTile<W: LayoutElement> {
tile: Tile<W>,
/// Width of the column the tile was in.
width: ColumnWidth,
/// Whether the column the tile was in was full-width.
is_full_width: bool,
2024-11-29 21:11:02 +03:00
/// Whether the tile was floating.
is_floating: bool,
2024-10-14 18:08:44 +03:00
}
/// Whether to activate a newly added window.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ActivateWindow {
/// Activate unconditionally.
Yes,
/// Activate based on heuristics.
#[default]
Smart,
/// Do not activate.
No,
}
2024-07-15 15:51:48 +02:00
impl<W: LayoutElement> InteractiveMoveState<W> {
fn moving(&self) -> Option<&InteractiveMoveData<W>> {
match self {
InteractiveMoveState::Moving(move_) => Some(move_),
_ => None,
}
}
}
impl<W: LayoutElement> InteractiveMoveData<W> {
fn tile_render_location(&self) -> Point<f64, Logical> {
let scale = Scale::from(self.output.current_scale().fractional_scale());
let window_size = self.tile.window_size();
let pointer_offset_within_window = Point::from((
window_size.w * self.pointer_ratio_within_window.0,
window_size.h * self.pointer_ratio_within_window.1,
));
let pos =
self.pointer_pos_within_output - pointer_offset_within_window - self.tile.window_loc()
+ self.tile.render_offset();
// Round to physical pixels.
pos.to_physical_precise_round(scale).to_logical(scale)
}
}
impl ActivateWindow {
pub fn map_smart(self, f: impl FnOnce() -> bool) -> bool {
match self {
ActivateWindow::Yes => true,
ActivateWindow::Smart => f(),
ActivateWindow::No => false,
}
}
}
impl Options {
fn from_config(config: &Config) -> Self {
let layout = &config.layout;
2024-09-05 23:37:10 +02:00
let preset_column_widths = if layout.preset_column_widths.is_empty() {
Options::default().preset_column_widths
} else {
2024-09-05 23:37:10 +02:00
layout
.preset_column_widths
.iter()
.copied()
.map(ColumnWidth::from)
.collect()
};
2024-09-05 23:37:10 +02:00
let preset_window_heights = if layout.preset_window_heights.is_empty() {
Options::default().preset_window_heights
} else {
layout.preset_window_heights.clone()
};
2023-11-02 21:07:29 +04:00
// Missing default_column_width maps to Some(ColumnWidth::Proportion(0.5)),
// while present, but empty, maps to None.
2024-09-05 23:37:10 +02:00
let default_column_width = layout
2023-11-02 21:07:29 +04:00
.default_column_width
.as_ref()
.map(|w| w.0.map(ColumnWidth::from))
2023-11-02 21:07:29 +04:00
.unwrap_or(Some(ColumnWidth::Proportion(0.5)));
Self {
2024-06-17 09:16:28 +03:00
gaps: layout.gaps.0,
struts: layout.struts,
focus_ring: layout.focus_ring,
border: layout.border,
2024-07-15 15:51:48 +02:00
insert_hint: layout.insert_hint,
2024-01-08 17:17:19 +04:00
center_focused_column: layout.center_focused_column,
always_center_single_column: layout.always_center_single_column,
empty_workspace_above_first: layout.empty_workspace_above_first,
2024-09-05 23:37:10 +02:00
preset_column_widths,
default_column_width,
2024-04-21 20:10:35 +04:00
animations: config.animations.clone(),
2024-08-22 14:36:47 +03:00
disable_resize_throttling: config.debug.disable_resize_throttling,
2024-08-22 14:44:11 +03:00
disable_transactions: config.debug.disable_transactions,
2024-09-05 23:37:10 +02:00
preset_window_heights,
}
}
2024-06-17 09:16:28 +03:00
fn adjusted_for_scale(mut self, scale: f64) -> Self {
let round = |logical: f64| round_logical_in_physical_max1(scale, logical);
self.gaps = round(self.gaps);
self.focus_ring.width = FloatOrInt(round(self.focus_ring.width.0));
self.border.width = FloatOrInt(round(self.border.width.0));
self
}
}
impl<W: LayoutElement> Layout<W> {
pub fn new(clock: Clock, config: &Config) -> Self {
Self::with_options_and_workspaces(clock, config, Options::from_config(config))
2024-02-06 19:09:15 +04:00
}
pub fn with_options(clock: Clock, options: Options) -> Self {
Self {
monitor_set: MonitorSet::NoOutputs { workspaces: vec![] },
is_active: true,
last_active_workspace_id: HashMap::new(),
2024-07-15 15:51:48 +02:00
interactive_move: None,
clock,
update_render_elements_time: Duration::ZERO,
2024-02-06 19:09:15 +04:00
options: Rc::new(options),
}
}
fn with_options_and_workspaces(clock: Clock, config: &Config, options: Options) -> Self {
2024-05-11 22:40:30 +02:00
let opts = Rc::new(options);
let workspaces = config
.workspaces
.iter()
.map(|ws| {
Workspace::new_with_config_no_outputs(Some(ws.clone()), clock.clone(), opts.clone())
})
2024-05-11 22:40:30 +02:00
.collect();
Self {
monitor_set: MonitorSet::NoOutputs { workspaces },
is_active: true,
last_active_workspace_id: HashMap::new(),
2024-07-15 15:51:48 +02:00
interactive_move: None,
clock,
update_render_elements_time: Duration::ZERO,
2024-05-11 22:40:30 +02:00
options: opts,
}
}
pub fn add_output(&mut self, output: Output) {
self.monitor_set = match mem::take(&mut self.monitor_set) {
MonitorSet::Normal {
mut monitors,
primary_idx,
active_monitor_idx,
} => {
let primary = &mut monitors[primary_idx];
let ws_id_to_activate = self.last_active_workspace_id.remove(&output.name());
let mut active_workspace_idx = None;
let mut stopped_primary_ws_switch = false;
let mut workspaces = vec![];
for i in (0..primary.workspaces.len()).rev() {
if primary.workspaces[i].original_output.matches(&output) {
let ws = primary.workspaces.remove(i);
// FIXME: this can be coded in a way that the workspace switch won't be
// affected if the removed workspace is invisible. But this is good enough
// for now.
if primary.workspace_switch.is_some() {
primary.workspace_switch = None;
stopped_primary_ws_switch = true;
}
// The user could've closed a window while remaining on this workspace, on
// another monitor. However, we will add an empty workspace in the end
// instead.
if ws.has_windows_or_name() {
if Some(ws.id()) == ws_id_to_activate {
active_workspace_idx = Some(workspaces.len());
}
workspaces.push(ws);
}
if i <= primary.active_workspace_idx
// Generally when moving the currently active workspace, we want to
// fall back to the workspace above, so as not to end up on the last
// empty workspace. However, with empty workspace above first, when
// moving the workspace at index 1 (first non-empty), we want to stay
// at index 1, so as once again not to end up on an empty workspace.
//
// This comes into play at compositor startup when having named
// workspaces set up across multiple monitors. Without this check, the
// first monitor to connect can end up with the first empty workspace
// focused instead of the first named workspace.
&& !(self.options.empty_workspace_above_first
&& primary.active_workspace_idx == 1)
{
primary.active_workspace_idx =
primary.active_workspace_idx.saturating_sub(1);
}
}
}
// If we stopped a workspace switch, then we might need to clean up workspaces.
// Also if empty_workspace_above_first is set and there are only 2 workspaces left,
// both will be empty and one of them needs to be removed. clean_up_workspaces
// takes care of this.
if stopped_primary_ws_switch
|| (primary.options.empty_workspace_above_first
&& primary.workspaces.len() == 2)
{
primary.clean_up_workspaces();
}
workspaces.reverse();
if let Some(idx) = &mut active_workspace_idx {
*idx = workspaces.len() - *idx - 1;
}
let mut active_workspace_idx = active_workspace_idx.unwrap_or(0);
// Make sure there's always an empty workspace.
workspaces.push(Workspace::new(
output.clone(),
self.clock.clone(),
self.options.clone(),
));
if self.options.empty_workspace_above_first && workspaces.len() > 1 {
workspaces.insert(
0,
Workspace::new(output.clone(), self.clock.clone(), self.options.clone()),
);
active_workspace_idx += 1;
}
for ws in &mut workspaces {
ws.set_output(Some(output.clone()));
}
let mut monitor =
Monitor::new(output, workspaces, self.clock.clone(), self.options.clone());
monitor.active_workspace_idx = active_workspace_idx;
monitors.push(monitor);
MonitorSet::Normal {
monitors,
primary_idx,
active_monitor_idx,
}
}
MonitorSet::NoOutputs { mut workspaces } => {
// We know there are no empty workspaces there, so add one.
workspaces.push(Workspace::new(
output.clone(),
self.clock.clone(),
self.options.clone(),
));
let mut active_workspace_idx = 0;
if self.options.empty_workspace_above_first && workspaces.len() > 1 {
workspaces.insert(
0,
Workspace::new(output.clone(), self.clock.clone(), self.options.clone()),
);
active_workspace_idx += 1;
}
let ws_id_to_activate = self.last_active_workspace_id.remove(&output.name());
for (i, workspace) in workspaces.iter_mut().enumerate() {
workspace.set_output(Some(output.clone()));
if Some(workspace.id()) == ws_id_to_activate {
active_workspace_idx = i;
}
}
let mut monitor =
Monitor::new(output, workspaces, self.clock.clone(), self.options.clone());
monitor.active_workspace_idx = active_workspace_idx;
MonitorSet::Normal {
monitors: vec![monitor],
primary_idx: 0,
active_monitor_idx: 0,
}
}
}
}
pub fn remove_output(&mut self, output: &Output) {
self.monitor_set = match mem::take(&mut self.monitor_set) {
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);
self.last_active_workspace_id.insert(
monitor.output_name().clone(),
monitor.workspaces[monitor.active_workspace_idx].id(),
);
let mut workspaces = monitor.workspaces;
for ws in &mut workspaces {
ws.set_output(None);
}
// Get rid of empty workspaces.
workspaces.retain(|ws| ws.has_windows_or_name());
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.set_output(Some(primary.output.clone()));
}
let mut stopped_primary_ws_switch = false;
if !workspaces.is_empty() && primary.workspace_switch.is_some() {
// FIXME: if we're adding workspaces to currently invisible positions
// (outside the workspace switch), we don't need to cancel it.
primary.workspace_switch = None;
stopped_primary_ws_switch = true;
}
let empty_was_focused =
primary.active_workspace_idx == primary.workspaces.len() - 1;
// Push the workspaces from the removed monitor in the end, right before the
// last, empty, workspace.
let empty = primary.workspaces.remove(primary.workspaces.len() - 1);
primary.workspaces.extend(workspaces);
primary.workspaces.push(empty);
// If empty_workspace_above_first is set and the first workspace is now no
// longer empty, add a new empty workspace on top.
if primary.options.empty_workspace_above_first
&& primary.workspaces[0].has_windows_or_name()
{
primary.add_workspace_top();
}
// If the empty workspace was focused on the primary monitor, keep it focused.
if empty_was_focused {
primary.active_workspace_idx = primary.workspaces.len() - 1;
}
if stopped_primary_ws_switch {
primary.clean_up_workspaces();
}
MonitorSet::Normal {
monitors,
primary_idx,
active_monitor_idx,
}
}
}
MonitorSet::NoOutputs { .. } => {
panic!("tried to remove output when there were already none")
}
}
}
2024-11-29 21:11:02 +03:00
// TODO: pos
pub fn add_window_by_idx(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
window: W,
activate: bool,
2023-11-01 17:41:02 +04:00
width: ColumnWidth,
is_full_width: bool,
2024-11-29 21:11:02 +03:00
is_floating: bool,
) {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
panic!()
};
2024-11-29 21:11:02 +03:00
monitors[monitor_idx].add_window(
workspace_idx,
window,
activate,
width,
is_full_width,
is_floating,
);
if activate {
*active_monitor_idx = monitor_idx;
}
}
2024-05-11 22:40:30 +02:00
/// Adds a new window to the layout on a specific workspace.
pub fn add_window_to_named_workspace(
&mut self,
workspace_name: &str,
window: W,
width: Option<ColumnWidth>,
is_full_width: bool,
2024-11-29 21:11:02 +03:00
is_floating: bool,
activate: ActivateWindow,
2024-05-11 22:40:30 +02:00
) -> Option<&Output> {
let width = self.resolve_default_width(&window, width);
2024-05-11 22:40:30 +02:00
match &mut self.monitor_set {
MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} => {
let (mon_idx, mon, ws_idx) = monitors
.iter_mut()
.enumerate()
.find_map(|(mon_idx, mon)| {
mon.find_named_workspace_index(workspace_name)
.map(move |ws_idx| (mon_idx, mon, ws_idx))
})
.unwrap();
if activate == ActivateWindow::Yes {
*active_monitor_idx = mon_idx;
2024-05-11 22:40:30 +02:00
}
let activate = activate.map_smart(|| {
// Don't steal focus from an active fullscreen window.
let ws = &mon.workspaces[ws_idx];
2024-11-30 09:18:33 +03:00
if mon_idx == *active_monitor_idx && ws.is_active_fullscreen() {
return false;
}
// Don't activate if on a different workspace.
if mon.active_workspace_idx != ws_idx {
return false;
}
true
});
2024-05-11 22:40:30 +02:00
2024-11-29 21:11:02 +03:00
mon.add_window(ws_idx, window, activate, width, is_full_width, is_floating);
2024-05-11 22:40:30 +02:00
Some(&mon.output)
}
MonitorSet::NoOutputs { workspaces } => {
let ws = workspaces
.iter_mut()
2024-05-16 10:54:24 +04:00
.find(|ws| {
ws.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
})
2024-05-11 22:40:30 +02:00
.unwrap();
let activate = activate.map_smart(|| true);
2024-11-29 21:11:02 +03:00
ws.add_window(window, activate, width, is_full_width, is_floating);
2024-05-11 22:40:30 +02:00
None
}
}
}
2024-01-15 10:36:59 +04:00
pub fn add_column_by_idx(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
column: Column<W>,
activate: bool,
) {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
panic!()
};
monitors[monitor_idx].add_column(workspace_idx, column, activate);
if activate {
*active_monitor_idx = monitor_idx;
}
}
/// Adds a new window to the layout.
///
/// Returns an output that the window was added to, if there were any outputs.
2023-11-02 21:07:29 +04:00
pub fn add_window(
&mut self,
window: W,
width: Option<ColumnWidth>,
is_full_width: bool,
2024-11-29 21:11:02 +03:00
is_floating: bool,
activate: ActivateWindow,
2023-11-02 21:07:29 +04:00
) -> Option<&Output> {
let width = self.resolve_default_width(&window, width);
2023-11-02 21:07:29 +04:00
match &mut self.monitor_set {
MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} => {
let mon = &mut monitors[*active_monitor_idx];
let activate = activate.map_smart(|| {
// Don't steal focus from an active fullscreen window.
let ws = &mon.workspaces[mon.active_workspace_idx];
2024-11-30 09:18:33 +03:00
!ws.is_active_fullscreen()
});
mon.add_window(
mon.active_workspace_idx,
window,
activate,
width,
is_full_width,
2024-11-29 21:11:02 +03:00
is_floating,
);
Some(&mon.output)
}
MonitorSet::NoOutputs { workspaces } => {
let ws = if let Some(ws) = workspaces.get_mut(0) {
ws
} else {
workspaces.push(Workspace::new_no_outputs(
self.clock.clone(),
self.options.clone(),
));
&mut workspaces[0]
};
let activate = activate.map_smart(|| true);
2024-11-29 21:11:02 +03:00
ws.add_window(window, activate, width, is_full_width, is_floating);
None
}
}
}
/// Adds a new window to the layout immediately to the right of another window.
///
/// If that another window was active, activates the new window.
///
/// Returns an output that the window was added to, if there were any outputs.
pub fn add_window_right_of(
&mut self,
right_of: &W::Id,
window: W,
width: Option<ColumnWidth>,
is_full_width: bool,
2024-11-29 21:11:02 +03:00
is_floating: bool,
) -> Option<&Output> {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if right_of == move_.tile.window().id() {
let output = move_.output.clone();
let activate = ActivateWindow::default();
2024-07-15 15:51:48 +02:00
if self.monitor_for_output(&output).is_some() {
2024-11-29 21:11:02 +03:00
self.add_window_on_output(
&output,
window,
width,
is_full_width,
is_floating,
activate,
);
2024-07-15 15:51:48 +02:00
return Some(&self.monitor_for_output(&output).unwrap().output);
} else {
2024-11-29 21:11:02 +03:00
return self.add_window(window, width, is_full_width, is_floating, activate);
2024-07-15 15:51:48 +02:00
}
}
}
let width = self.resolve_default_width(&window, width);
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
let mon = monitors
.iter_mut()
.find(|mon| mon.workspaces.iter().any(|ws| ws.has_window(right_of)))
.unwrap();
2024-11-29 21:11:02 +03:00
mon.add_window_right_of(right_of, window, width, is_full_width, is_floating);
Some(&mon.output)
}
MonitorSet::NoOutputs { workspaces } => {
let ws = workspaces
.iter_mut()
.find(|ws| ws.has_window(right_of))
.unwrap();
2024-11-29 21:11:02 +03:00
ws.add_window_right_of(right_of, window, width, is_full_width, is_floating);
None
}
}
}
2024-02-13 17:46:37 +04:00
/// Adds a new window to the layout on a specific output.
pub fn add_window_on_output(
&mut self,
output: &Output,
window: W,
width: Option<ColumnWidth>,
2024-02-13 17:46:37 +04:00
is_full_width: bool,
2024-11-29 21:11:02 +03:00
is_floating: bool,
activate: ActivateWindow,
2024-02-13 17:46:37 +04:00
) {
let width = self.resolve_default_width(&window, width);
2024-02-13 17:46:37 +04:00
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
panic!()
};
let (mon_idx, mon) = monitors
.iter_mut()
.enumerate()
.find(|(_, mon)| mon.output == *output)
.unwrap();
if activate == ActivateWindow::Yes {
*active_monitor_idx = mon_idx;
2024-02-13 17:46:37 +04:00
}
let activate = activate.map_smart(|| {
// Don't steal focus from an active fullscreen window.
let ws = &mon.workspaces[mon.active_workspace_idx];
2024-11-30 09:18:33 +03:00
mon_idx != *active_monitor_idx || !ws.is_active_fullscreen()
});
2024-02-13 17:46:37 +04:00
mon.add_window(
mon.active_workspace_idx,
window,
activate,
width,
is_full_width,
2024-11-29 21:11:02 +03:00
is_floating,
2024-02-13 17:46:37 +04:00
);
}
2024-10-14 18:08:44 +03:00
pub fn remove_window(
&mut self,
window: &W::Id,
transaction: Transaction,
) -> Option<RemovedTile<W>> {
2024-07-15 15:51:48 +02:00
if let Some(state) = &self.interactive_move {
match state {
InteractiveMoveState::Starting { window_id, .. } => {
if window_id == window {
self.interactive_move_end(window);
}
}
InteractiveMoveState::Moving(move_) => {
if move_.tile.window().id() == window {
let Some(InteractiveMoveState::Moving(move_)) =
self.interactive_move.take()
else {
unreachable!()
};
return Some(RemovedTile {
tile: move_.tile,
width: move_.width,
is_full_width: move_.is_full_width,
2024-11-29 21:11:02 +03:00
is_floating: false,
2024-07-15 15:51:48 +02:00
});
}
}
}
}
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for (idx, ws) in mon.workspaces.iter_mut().enumerate() {
if ws.has_window(window) {
2024-10-14 18:08:44 +03:00
let removed = ws.remove_tile(window, transaction);
// Clean up empty workspaces that are not active and not last.
if !ws.has_windows_or_name()
&& idx != mon.active_workspace_idx
&& idx != mon.workspaces.len() - 1
&& mon.workspace_switch.is_none()
{
mon.workspaces.remove(idx);
if idx < mon.active_workspace_idx {
mon.active_workspace_idx -= 1;
}
}
// Special case handling when empty_workspace_above_first is set and all
// workspaces are empty.
if mon.options.empty_workspace_above_first
&& mon.workspaces.len() == 2
&& mon.workspace_switch.is_none()
{
assert!(!mon.workspaces[0].has_windows_or_name());
assert!(!mon.workspaces[1].has_windows_or_name());
mon.workspaces.remove(1);
mon.active_workspace_idx = 0;
}
2024-10-14 18:08:44 +03:00
return Some(removed);
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
for (idx, ws) in workspaces.iter_mut().enumerate() {
if ws.has_window(window) {
2024-10-14 18:08:44 +03:00
let removed = ws.remove_tile(window, transaction);
// Clean up empty workspaces.
if !ws.has_windows_or_name() {
workspaces.remove(idx);
}
2024-10-14 18:08:44 +03:00
return Some(removed);
}
}
}
}
2024-08-23 15:41:06 +03:00
None
}
2024-05-10 16:58:53 +04:00
pub fn update_window(&mut self, window: &W::Id, serial: Option<Serial>) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if move_.tile.window().id() == window {
move_.tile.update_window();
return;
}
}
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
if ws.has_window(window) {
2024-05-10 16:58:53 +04:00
ws.update_window(window, serial);
2023-08-16 09:08:10 +04:00
return;
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
if ws.has_window(window) {
2024-05-10 16:58:53 +04:00
ws.update_window(window, serial);
2023-08-16 09:08:10 +04:00
return;
}
}
}
}
}
pub fn find_window_and_output(&self, wl_surface: &WlSurface) -> Option<(&W, &Output)> {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().is_wl_surface(wl_surface) {
return Some((move_.tile.window(), &move_.output));
}
}
if let MonitorSet::Normal { monitors, .. } = &self.monitor_set {
for mon in monitors {
for ws in &mon.workspaces {
if let Some(window) = ws.find_wl_surface(wl_surface) {
return Some((window, &mon.output));
}
}
}
}
None
}
2024-09-02 09:20:23 +03:00
pub fn find_workspace_by_id(&self, id: WorkspaceId) -> Option<(usize, &Workspace<W>)> {
match &self.monitor_set {
MonitorSet::Normal { ref monitors, .. } => {
for mon in monitors {
if let Some((index, workspace)) = mon
.workspaces
.iter()
.enumerate()
.find(|(_, w)| w.id() == id)
{
return Some((index, workspace));
}
}
}
MonitorSet::NoOutputs { workspaces } => {
if let Some((index, workspace)) =
workspaces.iter().enumerate().find(|(_, w)| w.id() == id)
{
return Some((index, workspace));
}
}
}
None
}
2024-05-11 22:40:30 +02:00
pub fn find_workspace_by_name(&self, workspace_name: &str) -> Option<(usize, &Workspace<W>)> {
match &self.monitor_set {
MonitorSet::Normal { ref monitors, .. } => {
for mon in monitors {
2024-05-16 10:54:24 +04:00
if let Some((index, workspace)) =
mon.workspaces.iter().enumerate().find(|(_, w)| {
w.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
})
2024-05-11 22:40:30 +02:00
{
return Some((index, workspace));
}
}
}
MonitorSet::NoOutputs { workspaces } => {
2024-05-16 10:54:24 +04:00
if let Some((index, workspace)) = workspaces.iter().enumerate().find(|(_, w)| {
w.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
}) {
2024-05-11 22:40:30 +02:00
return Some((index, workspace));
}
}
}
None
}
pub fn unname_workspace(&mut self, workspace_name: &str) {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
if mon.unname_workspace(workspace_name) {
if mon.workspace_switch.is_none() {
mon.clean_up_workspaces();
}
return;
}
}
}
MonitorSet::NoOutputs { workspaces } => {
for (idx, ws) in workspaces.iter_mut().enumerate() {
2024-05-16 10:54:24 +04:00
if ws
.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
{
2024-05-11 22:40:30 +02:00
ws.unname();
// Clean up empty workspaces.
if !ws.has_windows() {
workspaces.remove(idx);
}
return;
}
}
}
}
}
pub fn find_window_and_output_mut(
&mut self,
wl_surface: &WlSurface,
) -> Option<(&mut W, Option<&Output>)> {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if move_.tile.window().is_wl_surface(wl_surface) {
return Some((move_.tile.window_mut(), Some(&move_.output)));
}
}
2024-03-19 14:41:17 +04:00
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
if let Some(window) = ws.find_wl_surface_mut(wl_surface) {
return Some((window, Some(&mon.output)));
2024-03-19 14:41:17 +04:00
}
}
}
}
MonitorSet::NoOutputs { workspaces } => {
for ws in workspaces {
if let Some(window) = ws.find_wl_surface_mut(wl_surface) {
return Some((window, None));
}
}
}
}
None
}
2024-11-30 09:18:33 +03:00
/// Computes the window-geometry-relative target rect for popup unconstraining.
///
/// We will try to fit popups inside this rect.
pub fn popup_target_rect(&self, window: &W::Id) -> Rectangle<f64, Logical> {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().id() == window {
2024-11-30 09:18:33 +03:00
// Follow the scrolling layout logic and fit the popup horizontally within the
// window geometry.
let width = move_.tile.window_size().w;
let height = output_size(&move_.output).h;
let mut target = Rectangle::from_loc_and_size((0., 0.), (width, height));
// FIXME: ideally this shouldn't include the tile render offset, but the code
// duplication would be a bit annoying for this edge case.
target.loc.y -= move_.tile_render_location().y;
target.loc.y -= move_.tile.window_loc().y;
return target;
2023-12-19 20:56:00 +04:00
}
}
2024-11-30 09:18:33 +03:00
self.workspaces()
.find_map(|(_, _, ws)| ws.popup_target_rect(window))
.unwrap()
2023-12-19 20:56:00 +04:00
}
2023-09-26 20:12:04 +04:00
pub fn update_output_size(&mut self, output: &Output) {
2024-01-16 09:45:47 +04:00
let _span = tracy_client::span!("Layout::update_output_size");
let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else {
panic!()
};
for mon in monitors {
if &mon.output == output {
for ws in &mut mon.workspaces {
2024-11-30 09:18:33 +03:00
ws.update_output_size();
}
2023-09-26 20:12:04 +04:00
break;
}
}
}
pub fn scroll_amount_to_activate(&self, window: &W::Id) -> f64 {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().id() == window {
return 0.;
}
}
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return 0.;
};
for mon in monitors {
for ws in &mon.workspaces {
if ws.has_window(window) {
return ws.scroll_amount_to_activate(window);
}
}
}
0.
}
pub fn should_trigger_focus_follows_mouse_on(&self, window: &W::Id) -> bool {
// During an animation, it's easy to trigger focus-follows-mouse on the previous workspace,
// especially when clicking to switch workspace on a bar of some kind. This cancels the
// workspace switch, which is annoying and not intended.
//
// This function allows focus-follows-mouse to trigger only on the animation target
// workspace.
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().id() == window {
return true;
}
}
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return true;
};
let (mon, ws_idx) = monitors
.iter()
.find_map(|mon| {
mon.workspaces
.iter()
.position(|ws| ws.has_window(window))
.map(|ws_idx| (mon, ws_idx))
})
.unwrap();
// During a gesture, focus-follows-mouse does not cause any unintended workspace switches.
if let Some(WorkspaceSwitch::Gesture(_)) = mon.workspace_switch {
return true;
}
ws_idx == mon.active_workspace_idx
}
pub fn activate_window(&mut self, window: &W::Id) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().id() == window {
return;
}
}
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
return;
};
for (monitor_idx, mon) in monitors.iter_mut().enumerate() {
for (workspace_idx, ws) in mon.workspaces.iter_mut().enumerate() {
2024-12-08 09:15:10 +03:00
if ws.activate_window(window) {
*active_monitor_idx = monitor_idx;
// If currently in the middle of a vertical swipe between the target workspace
// and some other, don't switch the workspace.
match &mon.workspace_switch {
Some(WorkspaceSwitch::Gesture(gesture))
if gesture.current_idx.floor() == workspace_idx as f64
|| gesture.current_idx.ceil() == workspace_idx as f64 => {}
_ => mon.switch_workspace(workspace_idx),
}
2024-12-08 09:15:10 +03:00
return;
}
}
}
}
2024-11-29 21:11:02 +03:00
pub fn activate_window_without_raising(&mut self, window: &W::Id) {
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().id() == window {
return;
}
}
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
return;
};
for (monitor_idx, mon) in monitors.iter_mut().enumerate() {
for (workspace_idx, ws) in mon.workspaces.iter_mut().enumerate() {
if ws.activate_window_without_raising(window) {
*active_monitor_idx = monitor_idx;
// If currently in the middle of a vertical swipe between the target workspace
// and some other, don't switch the workspace.
match &mon.workspace_switch {
Some(WorkspaceSwitch::Gesture(gesture))
if gesture.current_idx.floor() == workspace_idx as f64
|| gesture.current_idx.ceil() == workspace_idx as f64 => {}
_ => mon.switch_workspace(workspace_idx),
}
return;
}
}
}
}
pub fn activate_output(&mut self, output: &Output) {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
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.monitor_set
else {
return None;
};
Some(&monitors[*active_monitor_idx].output)
}
pub fn active_workspace(&self) -> Option<&Workspace<W>> {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &self.monitor_set
else {
return None;
};
let mon = &monitors[*active_monitor_idx];
Some(&mon.workspaces[mon.active_workspace_idx])
}
pub fn active_workspace_mut(&mut self) -> Option<&mut Workspace<W>> {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
return None;
};
let mon = &mut monitors[*active_monitor_idx];
Some(&mut mon.workspaces[mon.active_workspace_idx])
}
2023-08-16 10:59:34 +04:00
pub fn windows_for_output(&self, output: &Output) -> impl Iterator<Item = &W> + '_ {
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
2023-08-16 10:59:34 +04:00
panic!()
};
2024-07-15 15:51:48 +02:00
let moving_window = self
.interactive_move
.as_ref()
.and_then(|x| x.moving())
.filter(|move_| move_.output == *output)
.map(|move_| move_.tile.window())
.into_iter();
2023-08-16 10:59:34 +04:00
let mon = monitors.iter().find(|mon| &mon.output == output).unwrap();
2024-07-15 15:51:48 +02:00
let mon_windows = mon.workspaces.iter().flat_map(|ws| ws.windows());
moving_window.chain(mon_windows)
2023-08-16 10:59:34 +04:00
}
2024-07-15 15:51:48 +02:00
pub fn with_windows(&self, mut f: impl FnMut(&W, Option<&Output>, Option<WorkspaceId>)) {
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
f(move_.tile.window(), Some(&move_.output), None);
}
2024-01-29 19:34:12 +04:00
match &self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mon.workspaces {
for win in ws.windows() {
2024-07-15 15:51:48 +02:00
f(win, Some(&mon.output), Some(ws.id()));
2024-01-29 19:34:12 +04:00
}
}
}
}
MonitorSet::NoOutputs { workspaces } => {
for ws in workspaces {
for win in ws.windows() {
2024-07-15 15:51:48 +02:00
f(win, None, Some(ws.id()));
2024-01-29 19:34:12 +04:00
}
}
}
}
}
pub fn with_windows_mut(&mut self, mut f: impl FnMut(&mut W, Option<&Output>)) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
f(move_.tile.window_mut(), Some(&move_.output));
}
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
for win in ws.windows_mut() {
f(win, Some(&mon.output));
}
}
}
}
MonitorSet::NoOutputs { workspaces } => {
for ws in workspaces {
for win in ws.windows_mut() {
f(win, None);
}
}
}
}
}
fn active_monitor(&mut self) -> Option<&mut Monitor<W>> {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
return None;
};
Some(&mut monitors[*active_monitor_idx])
}
pub fn active_monitor_ref(&self) -> Option<&Monitor<W>> {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &self.monitor_set
else {
return None;
};
Some(&monitors[*active_monitor_idx])
}
pub fn monitor_for_output(&self, output: &Output) -> Option<&Monitor<W>> {
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return None;
};
2024-07-15 15:51:48 +02:00
monitors.iter().find(|mon| &mon.output == output)
}
pub fn monitor_for_output_mut(&mut self, output: &Output) -> Option<&mut Monitor<W>> {
let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else {
return None;
};
monitors.iter_mut().find(|mon| &mon.output == output)
}
2024-05-11 22:40:30 +02:00
pub fn monitor_for_workspace(&self, workspace_name: &str) -> Option<&Monitor<W>> {
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return None;
};
monitors.iter().find(|monitor| {
2024-05-16 10:54:24 +04:00
monitor.workspaces.iter().any(|ws| {
ws.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(workspace_name))
})
2024-05-11 22:40:30 +02:00
})
}
2023-08-15 12:49:26 +04:00
pub fn outputs(&self) -> impl Iterator<Item = &Output> + '_ {
let monitors = if let MonitorSet::Normal { monitors, .. } = &self.monitor_set {
2023-08-15 12:49:26 +04:00
&monitors[..]
} else {
&[][..]
};
monitors.iter().map(|mon| &mon.output)
}
pub fn move_left(&mut self) {
2023-08-14 16:19:43 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.move_left();
}
pub fn move_right(&mut self) {
2023-08-14 16:19:43 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.move_right();
}
2023-12-29 08:01:02 +04:00
pub fn move_column_to_first(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.move_column_to_first();
}
pub fn move_column_to_last(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.move_column_to_last();
}
pub fn move_column_left_or_to_output(&mut self, output: &Output) -> bool {
if let Some(monitor) = self.active_monitor() {
2024-11-30 09:18:33 +03:00
if monitor.move_left() {
return false;
}
}
self.move_column_to_output(output);
true
}
pub fn move_column_right_or_to_output(&mut self, output: &Output) -> bool {
if let Some(monitor) = self.active_monitor() {
2024-11-30 09:18:33 +03:00
if monitor.move_right() {
return false;
}
}
self.move_column_to_output(output);
true
}
pub fn move_down(&mut self) {
2023-08-14 16:19:43 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.move_down();
}
pub fn move_up(&mut self) {
2023-08-14 16:19:43 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.move_up();
}
pub fn move_down_or_to_workspace_down(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.move_down_or_to_workspace_down();
}
pub fn move_up_or_to_workspace_up(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.move_up_or_to_workspace_up();
}
pub fn consume_or_expel_window_left(&mut self, window: Option<&W::Id>) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if window.is_none() || window == Some(move_.tile.window().id()) {
2024-07-15 15:51:48 +02:00
return;
}
}
let workspace = if let Some(window) = window {
Some(
self.workspaces_mut()
.find(|ws| ws.has_window(window))
.unwrap(),
)
} else {
self.active_workspace_mut()
};
let Some(workspace) = workspace else {
return;
};
workspace.consume_or_expel_window_left(window);
}
pub fn consume_or_expel_window_right(&mut self, window: Option<&W::Id>) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if window.is_none() || window == Some(move_.tile.window().id()) {
2024-07-15 15:51:48 +02:00
return;
}
}
let workspace = if let Some(window) = window {
Some(
self.workspaces_mut()
.find(|ws| ws.has_window(window))
.unwrap(),
)
} else {
self.active_workspace_mut()
};
let Some(workspace) = workspace else {
return;
};
workspace.consume_or_expel_window_right(window);
}
pub fn focus_left(&mut self) {
2023-08-14 16:19:43 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.focus_left();
}
pub fn focus_right(&mut self) {
2023-08-14 16:19:43 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.focus_right();
}
2023-12-29 07:51:14 +04:00
pub fn focus_column_first(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.focus_column_first();
}
pub fn focus_column_last(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.focus_column_last();
}
pub fn focus_column_right_or_first(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.focus_column_right_or_first();
}
pub fn focus_column_left_or_last(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.focus_column_left_or_last();
}
pub fn focus_window_up_or_output(&mut self, output: &Output) -> bool {
if let Some(monitor) = self.active_monitor() {
2024-11-30 09:18:33 +03:00
if monitor.focus_up() {
return false;
}
}
self.focus_output(output);
true
}
pub fn focus_window_down_or_output(&mut self, output: &Output) -> bool {
if let Some(monitor) = self.active_monitor() {
2024-11-30 09:18:33 +03:00
if monitor.focus_down() {
return false;
}
}
self.focus_output(output);
true
}
pub fn focus_column_left_or_output(&mut self, output: &Output) -> bool {
if let Some(monitor) = self.active_monitor() {
2024-11-30 09:18:33 +03:00
if monitor.focus_left() {
return false;
}
}
self.focus_output(output);
true
}
pub fn focus_column_right_or_output(&mut self, output: &Output) -> bool {
if let Some(monitor) = self.active_monitor() {
2024-11-30 09:18:33 +03:00
if monitor.focus_right() {
return false;
}
}
self.focus_output(output);
true
}
pub fn focus_down(&mut self) {
2023-08-14 16:19:43 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.focus_down();
}
pub fn focus_up(&mut self) {
2023-08-14 16:19:43 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.focus_up();
}
pub fn focus_down_or_left(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.focus_down_or_left();
}
pub fn focus_down_or_right(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.focus_down_or_right();
}
pub fn focus_up_or_left(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.focus_up_or_left();
}
pub fn focus_up_or_right(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.focus_up_or_right();
}
pub fn focus_window_or_workspace_down(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.focus_window_or_workspace_down();
}
pub fn focus_window_or_workspace_up(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.focus_window_or_workspace_up();
}
pub fn move_to_workspace_up(&mut self) {
2023-08-14 16:19:43 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.move_to_workspace_up();
}
pub fn move_to_workspace_down(&mut self) {
2023-08-14 16:19:43 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.move_to_workspace_down();
}
pub fn move_to_workspace(&mut self, window: Option<&W::Id>, idx: usize) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if window.is_none() || window == Some(move_.tile.window().id()) {
2024-07-15 15:51:48 +02:00
return;
}
}
2023-09-16 12:14:02 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.move_to_workspace(window, idx);
2024-05-11 22:40:30 +02:00
}
2024-01-15 10:31:44 +04:00
pub fn move_column_to_workspace_up(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.move_column_to_workspace_up();
}
pub fn move_column_to_workspace_down(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.move_column_to_workspace_down();
}
pub fn move_column_to_workspace(&mut self, idx: usize) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.move_column_to_workspace(idx);
}
2024-05-11 22:40:30 +02:00
pub fn move_column_to_workspace_on_output(&mut self, output: &Output, idx: usize) {
self.move_column_to_output(output);
2024-05-11 22:40:30 +02:00
self.focus_output(output);
self.move_column_to_workspace(idx);
}
pub fn switch_workspace_up(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.switch_workspace_up();
}
pub fn switch_workspace_down(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.switch_workspace_down();
}
pub fn switch_workspace(&mut self, idx: usize) {
2023-09-16 12:14:02 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.switch_workspace(idx);
2023-09-16 12:14:02 +04:00
}
2024-03-19 14:27:52 +00:00
pub fn switch_workspace_auto_back_and_forth(&mut self, idx: usize) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.switch_workspace_auto_back_and_forth(idx);
}
pub fn switch_workspace_previous(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.switch_workspace_previous();
}
pub fn consume_into_column(&mut self) {
2023-08-14 16:19:43 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.consume_into_column();
}
pub fn expel_from_column(&mut self) {
2023-08-14 16:19:43 +04:00
let Some(monitor) = self.active_monitor() else {
return;
};
2023-08-14 16:19:43 +04:00
monitor.expel_from_column();
}
pub fn center_column(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.center_column();
}
pub fn focus(&self) -> Option<&W> {
2024-11-30 09:18:33 +03:00
self.focus_with_output().map(|(win, _out)| win)
}
pub fn focus_with_output(&self) -> Option<(&W, &Output)> {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
2024-11-30 09:18:33 +03:00
return Some((move_.tile.window(), &move_.output));
2024-07-15 15:51:48 +02:00
}
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &self.monitor_set
else {
return None;
};
2024-11-30 09:18:33 +03:00
let mon = &monitors[*active_monitor_idx];
mon.active_window().map(|win| (win, &mon.output))
2023-08-14 15:54:11 +04:00
}
/// Returns the window under the cursor and the position of its toplevel surface within the
/// output.
///
/// `Some((w, Some(p)))` means that the cursor is within the window's input region and can be
/// used for delivering events to the window. `Some((w, None))` means that the cursor is within
/// the window's activation region, but not within the window's input region. For example, the
/// cursor may be on the window's server-side border.
pub fn window_under(
&self,
output: &Output,
pos_within_output: Point<f64, Logical>,
2024-06-17 09:16:28 +03:00
) -> Option<(&W, Option<Point<f64, Logical>>)> {
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return None;
};
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
let tile_pos = move_.tile_render_location();
let pos_within_tile = pos_within_output - tile_pos;
if move_.tile.is_in_input_region(pos_within_tile) {
let pos_within_surface = tile_pos + move_.tile.buf_loc();
return Some((move_.tile.window(), Some(pos_within_surface)));
} else if move_.tile.is_in_activation_region(pos_within_tile) {
return Some((move_.tile.window(), None));
}
return None;
};
let mon = monitors.iter().find(|mon| &mon.output == output)?;
mon.window_under(pos_within_output)
}
2024-05-10 16:58:53 +04:00
pub fn resize_edges_under(
&self,
output: &Output,
pos_within_output: Point<f64, Logical>,
) -> Option<ResizeEdge> {
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return None;
};
let mon = monitors.iter().find(|mon| &mon.output == output)?;
mon.resize_edges_under(pos_within_output)
}
2023-09-12 19:44:17 +04:00
#[cfg(test)]
fn verify_invariants(&self) {
2024-03-19 14:27:52 +00:00
use std::collections::HashSet;
2024-07-15 15:51:48 +02:00
use approx::assert_abs_diff_eq;
use crate::layout::monitor::WorkspaceSwitch;
2024-07-15 15:51:48 +02:00
let mut move_win_id = None;
if let Some(state) = &self.interactive_move {
match state {
InteractiveMoveState::Starting {
window_id,
pointer_delta: _,
pointer_ratio_within_window: _,
} => {
assert!(
self.has_window(window_id),
"interactive move must be on an existing window"
);
move_win_id = Some(window_id.clone());
}
InteractiveMoveState::Moving(move_) => {
assert_eq!(self.clock, move_.tile.clock);
2024-07-15 15:51:48 +02:00
let scale = move_.output.current_scale().fractional_scale();
let options = Options::clone(&self.options).adjusted_for_scale(scale);
assert_eq!(
&*move_.tile.options, &options,
"interactive moved tile options must be \
base options adjusted for output scale"
);
let tile_pos = move_.tile_render_location();
let rounded_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale);
// Tile position must be rounded to physical pixels.
assert_abs_diff_eq!(tile_pos.x, rounded_pos.x, epsilon = 1e-5);
assert_abs_diff_eq!(tile_pos.y, rounded_pos.y, epsilon = 1e-5);
}
}
}
2024-03-19 14:27:52 +00:00
let mut seen_workspace_id = HashSet::new();
2024-05-16 10:54:24 +04:00
let mut seen_workspace_name = Vec::<String>::new();
2024-03-19 14:27:52 +00:00
let (monitors, &primary_idx, &active_monitor_idx) = match &self.monitor_set {
MonitorSet::Normal {
monitors,
primary_idx,
active_monitor_idx,
} => (monitors, primary_idx, active_monitor_idx),
MonitorSet::NoOutputs { workspaces } => {
for workspace in workspaces {
assert!(
workspace.has_windows_or_name(),
2024-05-11 22:40:30 +02:00
"with no outputs there cannot be empty unnamed workspaces"
);
assert_eq!(self.clock, workspace.clock);
assert_eq!(
2024-06-17 09:16:28 +03:00
workspace.base_options, self.options,
"workspace base options must be synchronized with layout"
);
let options = Options::clone(&workspace.base_options)
.adjusted_for_scale(workspace.scale().fractional_scale());
assert_eq!(
&*workspace.options, &options,
"workspace options must be base options adjusted for workspace scale"
);
2024-03-19 14:27:52 +00:00
assert!(
seen_workspace_id.insert(workspace.id()),
"workspace id must be unique"
);
2024-05-11 22:40:30 +02:00
if let Some(name) = &workspace.name {
assert!(
2024-05-16 10:54:24 +04:00
!seen_workspace_name
.iter()
.any(|n| n.eq_ignore_ascii_case(name)),
2024-05-11 22:40:30 +02:00
"workspace name must be unique"
);
2024-05-16 10:54:24 +04:00
seen_workspace_name.push(name.clone());
2024-05-11 22:40:30 +02:00
}
2024-07-15 15:51:48 +02:00
workspace.verify_invariants(move_win_id.as_ref());
}
return;
}
};
2023-09-26 20:35:08 +04:00
assert!(primary_idx < monitors.len());
assert!(active_monitor_idx < monitors.len());
for (idx, monitor) in monitors.iter().enumerate() {
assert!(
!monitor.workspaces.is_empty(),
2023-10-14 19:24:12 +04:00
"monitor must have at least one workspace"
);
2023-09-26 20:35:08 +04:00
assert!(monitor.active_workspace_idx < monitor.workspaces.len());
assert_eq!(self.clock, monitor.clock);
assert_eq!(
monitor.options, self.options,
"monitor options must be synchronized with layout"
);
if let Some(WorkspaceSwitch::Animation(anim)) = &monitor.workspace_switch {
let before_idx = anim.from() as usize;
let after_idx = anim.to() as usize;
assert!(before_idx < monitor.workspaces.len());
assert!(after_idx < monitor.workspaces.len());
}
if idx == primary_idx {
for ws in &monitor.workspaces {
if ws.original_output.matches(&monitor.output) {
// This is the primary monitor's own workspace.
continue;
}
let own_monitor_exists = monitors
.iter()
.any(|m| ws.original_output.matches(&m.output));
assert!(
!own_monitor_exists,
"primary monitor cannot have workspaces for which their own monitor exists"
);
}
} else {
assert!(
monitor
.workspaces
.iter()
.any(|workspace| workspace.original_output.matches(&monitor.output)),
"secondary monitor must not have any non-own workspaces"
);
}
assert!(
2024-11-30 09:18:33 +03:00
!monitor.workspaces.last().unwrap().has_windows(),
"monitor must have an empty workspace in the end"
);
if monitor.options.empty_workspace_above_first {
assert!(
2024-11-30 09:18:33 +03:00
!monitor.workspaces.first().unwrap().has_windows(),
"first workspace must be empty when empty_workspace_above_first is set"
)
}
2024-05-11 22:40:30 +02:00
assert!(
monitor.workspaces.last().unwrap().name.is_none(),
"monitor must have an unnamed workspace in the end"
);
if monitor.options.empty_workspace_above_first {
assert!(
monitor.workspaces.first().unwrap().name.is_none(),
"first workspace must be unnamed when empty_workspace_above_first is set"
)
}
if monitor.options.empty_workspace_above_first {
assert!(
monitor.workspaces.len() != 2,
"if empty_workspace_above_first is set there must be just 1 or 3+ workspaces"
)
}
2024-05-11 22:40:30 +02:00
2023-10-14 20:39:44 +04:00
// If there's no workspace switch in progress, there can't be any non-last non-active
// empty workspaces. If empty_workspace_above_first is set then the first workspace
// will be empty too.
let pre_skip = if monitor.options.empty_workspace_above_first {
1
} else {
0
};
2023-10-14 20:39:44 +04:00
if monitor.workspace_switch.is_none() {
for (idx, ws) in monitor
.workspaces
.iter()
.enumerate()
.skip(pre_skip)
.rev()
// skip last
.skip(1)
{
2023-10-14 20:39:44 +04:00
if idx != monitor.active_workspace_idx {
assert!(
ws.has_windows_or_name(),
2024-05-11 22:40:30 +02:00
"non-active workspace can't be empty and unnamed except the last one"
2023-10-14 20:39:44 +04:00
);
}
}
}
// FIXME: verify that primary doesn't have any workspaces for which their own monitor
// exists.
for workspace in &monitor.workspaces {
assert_eq!(self.clock, workspace.clock);
assert_eq!(
2024-06-17 09:16:28 +03:00
workspace.base_options, self.options,
"workspace options must be synchronized with layout"
);
2024-06-17 09:16:28 +03:00
let options = Options::clone(&workspace.base_options)
.adjusted_for_scale(workspace.scale().fractional_scale());
assert_eq!(
&*workspace.options, &options,
"workspace options must be base options adjusted for workspace scale"
);
2024-03-19 14:27:52 +00:00
assert!(
seen_workspace_id.insert(workspace.id()),
"workspace id must be unique"
);
2024-05-11 22:40:30 +02:00
if let Some(name) = &workspace.name {
assert!(
2024-05-16 10:54:24 +04:00
!seen_workspace_name
.iter()
.any(|n| n.eq_ignore_ascii_case(name)),
2024-05-11 22:40:30 +02:00
"workspace name must be unique"
);
2024-05-16 10:54:24 +04:00
seen_workspace_name.push(name.clone());
2024-05-11 22:40:30 +02:00
}
2024-07-15 15:51:48 +02:00
workspace.verify_invariants(move_win_id.as_ref());
}
}
}
2023-08-14 17:40:15 +04:00
pub fn advance_animations(&mut self) {
2023-10-11 14:53:53 +04:00
let _span = tracy_client::span!("Layout::advance_animations");
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
move_.tile.advance_animations();
2024-07-15 15:51:48 +02:00
}
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
mon.advance_animations();
2023-08-14 17:40:15 +04:00
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
2023-08-14 17:40:15 +04:00
for ws in workspaces {
ws.advance_animations();
2023-08-14 17:40:15 +04:00
}
}
}
}
2023-08-14 18:29:50 +04:00
2024-07-15 15:51:48 +02:00
pub fn are_animations_ongoing(&self, output: Option<&Output>) -> bool {
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.are_animations_ongoing() {
return true;
}
}
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return false;
};
for mon in monitors {
if output.map_or(false, |output| mon.output != *output) {
continue;
}
if mon.are_animations_ongoing() {
return true;
}
}
false
}
2024-08-23 12:48:05 +03:00
pub fn update_render_elements(&mut self, output: Option<&Output>) {
let _span = tracy_client::span!("Layout::update_render_elements");
self.update_render_elements_time = self.clock.now();
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if output.map_or(true, |output| move_.output == *output) {
let pos_within_output = move_.tile_render_location();
let view_rect = Rectangle::from_loc_and_size(
pos_within_output.upscale(-1.),
output_size(&move_.output),
);
move_.tile.update(true, view_rect);
}
}
self.update_insert_hint(output);
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
error!("update_render_elements called with no monitors");
return;
};
for (idx, mon) in monitors.iter_mut().enumerate() {
2024-08-23 12:48:05 +03:00
if output.map_or(true, |output| mon.output == *output) {
2024-07-15 15:51:48 +02:00
let is_active = self.is_active
&& idx == *active_monitor_idx
&& !matches!(self.interactive_move, Some(InteractiveMoveState::Moving(_)));
mon.update_render_elements(is_active);
}
}
}
pub fn update_shaders(&mut self) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
move_.tile.update_shaders();
}
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
ws.update_shaders();
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
ws.update_shaders();
}
}
}
}
2024-07-15 15:51:48 +02:00
fn update_insert_hint(&mut self, output: Option<&Output>) {
let _span = tracy_client::span!("Layout::update_insert_hint");
let _span = tracy_client::span!("Layout::update_insert_hint::clear");
for ws in self.workspaces_mut() {
ws.clear_insert_hint();
}
if !matches!(self.interactive_move, Some(InteractiveMoveState::Moving(_))) {
return;
}
let Some(InteractiveMoveState::Moving(move_)) = self.interactive_move.take() else {
unreachable!()
};
if output.map_or(false, |out| &move_.output != out) {
self.interactive_move = Some(InteractiveMoveState::Moving(move_));
return;
}
// No insert hint when targeting floating.
if move_.is_floating {
self.interactive_move = Some(InteractiveMoveState::Moving(move_));
return;
}
2024-07-15 15:51:48 +02:00
let _span = tracy_client::span!("Layout::update_insert_hint::update");
if let Some(mon) = self.monitor_for_output_mut(&move_.output) {
if let Some((ws, offset)) = mon.workspace_under(move_.pointer_pos_within_output) {
let ws_id = ws.id();
let ws = mon
.workspaces
.iter_mut()
.find(|ws| ws.id() == ws_id)
.unwrap();
let position = ws.get_insert_position(move_.pointer_pos_within_output - offset);
2024-11-02 09:42:46 +03:00
let rules = move_.tile.window().rules();
let border_width = move_.tile.effective_border_width().unwrap_or(0.);
let corner_radius = rules
.geometry_corner_radius
.map_or(CornerRadius::default(), |radius| {
radius.expanded_by(border_width as f32)
});
2024-07-15 15:51:48 +02:00
ws.set_insert_hint(InsertHint {
position,
width: move_.width,
is_full_width: move_.is_full_width,
2024-11-02 09:42:46 +03:00
corner_radius,
2024-07-15 15:51:48 +02:00
});
}
}
self.interactive_move = Some(InteractiveMoveState::Moving(move_));
}
2024-05-11 22:40:30 +02:00
pub fn ensure_named_workspace(&mut self, ws_config: &WorkspaceConfig) {
if self.find_workspace_by_name(&ws_config.name.0).is_some() {
return;
}
let clock = self.clock.clone();
2024-05-11 22:40:30 +02:00
let options = self.options.clone();
match &mut self.monitor_set {
MonitorSet::Normal {
monitors,
primary_idx,
active_monitor_idx,
} => {
let mon_idx = ws_config
.open_on_output
.as_deref()
.map(|name| {
monitors
.iter_mut()
.position(|monitor| output_matches_name(&monitor.output, name))
2024-05-11 22:40:30 +02:00
.unwrap_or(*primary_idx)
})
.unwrap_or(*active_monitor_idx);
let mon = &mut monitors[mon_idx];
let mut insert_idx = 0;
if mon.options.empty_workspace_above_first {
// need to insert new empty workspace on top
mon.add_workspace_top();
insert_idx += 1;
}
2024-05-11 22:40:30 +02:00
let ws = Workspace::new_with_config(
mon.output.clone(),
Some(ws_config.clone()),
clock,
2024-05-11 22:40:30 +02:00
options,
);
mon.workspaces.insert(insert_idx, ws);
2024-05-11 22:40:30 +02:00
mon.active_workspace_idx += 1;
2024-05-11 22:40:30 +02:00
mon.workspace_switch = None;
mon.clean_up_workspaces();
}
MonitorSet::NoOutputs { workspaces } => {
let ws =
Workspace::new_with_config_no_outputs(Some(ws_config.clone()), clock, options);
2024-05-11 22:40:30 +02:00
workspaces.insert(0, ws);
}
}
}
2023-09-26 13:09:33 +04:00
pub fn update_config(&mut self, config: &Config) {
2024-11-26 21:23:20 +03:00
self.update_options(Options::from_config(config));
}
fn update_options(&mut self, options: Options) {
let options = Rc::new(options);
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
let scale = move_.output.current_scale().fractional_scale();
move_.tile.update_config(
scale,
Rc::new(Options::clone(&options).adjusted_for_scale(scale)),
);
2024-07-15 15:51:48 +02:00
}
match &mut self.monitor_set {
2023-09-26 13:09:33 +04:00
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
mon.update_config(options.clone());
2023-09-26 13:09:33 +04:00
}
}
MonitorSet::NoOutputs { workspaces } => {
2023-09-26 13:09:33 +04:00
for ws in workspaces {
ws.update_config(options.clone());
2023-09-26 13:09:33 +04:00
}
}
}
self.options = options;
2023-09-26 13:09:33 +04:00
}
2023-08-14 18:29:50 +04:00
pub fn toggle_width(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.toggle_width();
}
2023-08-14 18:34:39 +04:00
2024-09-12 11:53:10 +03:00
pub fn toggle_window_height(&mut self, window: Option<&W::Id>) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if window.is_none() || window == Some(move_.tile.window().id()) {
2024-07-15 15:51:48 +02:00
return;
}
}
2024-09-12 11:53:10 +03:00
let workspace = if let Some(window) = window {
Some(
self.workspaces_mut()
.find(|ws| ws.has_window(window))
.unwrap(),
)
} else {
self.active_workspace_mut()
};
let Some(workspace) = workspace else {
2024-09-05 23:37:10 +02:00
return;
};
2024-09-12 11:53:10 +03:00
workspace.toggle_window_height(window);
2024-09-05 23:37:10 +02:00
}
2023-08-14 18:34:39 +04:00
pub fn toggle_full_width(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.toggle_full_width();
}
2023-08-16 08:03:20 +04:00
2023-10-03 11:38:42 +04:00
pub fn set_column_width(&mut self, change: SizeChange) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.set_column_width(change);
}
pub fn set_window_height(&mut self, window: Option<&W::Id>, change: SizeChange) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if window.is_none() || window == Some(move_.tile.window().id()) {
2024-07-15 15:51:48 +02:00
return;
}
}
let workspace = if let Some(window) = window {
Some(
self.workspaces_mut()
.find(|ws| ws.has_window(window))
.unwrap(),
)
} else {
self.active_workspace_mut()
};
let Some(workspace) = workspace else {
2023-11-08 11:17:06 +04:00
return;
};
workspace.set_window_height(window, change);
2023-11-08 11:17:06 +04:00
}
pub fn reset_window_height(&mut self, window: Option<&W::Id>) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if window.is_none() || window == Some(move_.tile.window().id()) {
2024-07-15 15:51:48 +02:00
return;
}
}
let workspace = if let Some(window) = window {
Some(
self.workspaces_mut()
.find(|ws| ws.has_window(window))
.unwrap(),
)
} else {
self.active_workspace_mut()
};
let Some(workspace) = workspace else {
2024-05-11 09:33:23 +04:00
return;
};
workspace.reset_window_height(window);
2024-05-11 09:33:23 +04:00
}
2024-11-29 21:11:02 +03:00
pub fn toggle_window_floating(&mut self, window: Option<&W::Id>) {
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if window.is_none() || window == Some(move_.tile.window().id()) {
move_.is_floating = !move_.is_floating;
2024-11-29 21:11:02 +03:00
return;
}
}
let workspace = if let Some(window) = window {
Some(
self.workspaces_mut()
.find(|ws| ws.has_window(window))
.unwrap(),
)
} else {
self.active_workspace_mut()
};
let Some(workspace) = workspace else {
return;
};
workspace.toggle_window_floating(window);
}
pub fn switch_focus_floating_tiling(&mut self) {
let Some(workspace) = self.active_workspace_mut() else {
return;
};
workspace.switch_focus_floating_tiling();
}
2023-08-16 08:03:20 +04:00
pub fn focus_output(&mut self, output: &Output) {
if let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
2023-08-16 08:03:20 +04:00
{
for (idx, mon) in monitors.iter().enumerate() {
if &mon.output == output {
*active_monitor_idx = idx;
return;
}
}
}
}
pub fn move_to_output(
&mut self,
window: Option<&W::Id>,
output: &Output,
target_ws_idx: Option<usize>,
) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if window.is_none() || window == Some(move_.tile.window().id()) {
2024-07-15 15:51:48 +02:00
return;
}
}
2023-08-16 08:03:20 +04:00
if let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
2023-08-16 08:03:20 +04:00
{
let new_idx = monitors
.iter()
.position(|mon| &mon.output == output)
.unwrap();
2024-11-30 09:18:33 +03:00
let (mon_idx, ws_idx) = if let Some(window) = window {
monitors
.iter()
.enumerate()
.find_map(|(mon_idx, mon)| {
2024-11-30 09:18:33 +03:00
mon.workspaces
.iter()
.position(|ws| ws.has_window(window))
.map(|ws_idx| (mon_idx, ws_idx))
})
.unwrap()
} else {
let mon_idx = *active_monitor_idx;
let mon = &monitors[mon_idx];
2024-11-30 09:18:33 +03:00
(mon_idx, mon.active_workspace_idx)
};
let workspace_idx = target_ws_idx.unwrap_or(monitors[new_idx].active_workspace_idx);
if mon_idx == new_idx && ws_idx == workspace_idx {
2023-08-16 08:03:20 +04:00
return;
}
let mon = &mut monitors[mon_idx];
2024-11-30 09:18:33 +03:00
let activate = window.map_or(true, |win| {
mon_idx == *active_monitor_idx
&& mon.active_window().map(|win| win.id()) == Some(win)
});
2024-11-30 09:18:33 +03:00
let ws = &mut mon.workspaces[ws_idx];
let transaction = Transaction::new();
let removed = if let Some(window) = window {
ws.remove_tile(window, transaction)
} else if let Some(removed) = ws.remove_active_tile(transaction) {
removed
} else {
return;
};
2023-08-16 08:03:20 +04:00
self.add_window_by_idx(
new_idx,
workspace_idx,
2024-10-14 18:08:44 +03:00
removed.tile.into_window(),
activate,
2024-10-14 18:08:44 +03:00
removed.width,
removed.is_full_width,
2024-11-29 21:11:02 +03:00
removed.is_floating,
);
let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else {
unreachable!()
};
let mon = &mut monitors[mon_idx];
if mon.workspace_switch.is_none() {
monitors[mon_idx].clean_up_workspaces();
}
2023-08-16 08:03:20 +04:00
}
}
2023-08-16 09:08:10 +04:00
2024-01-15 10:36:59 +04:00
pub fn move_column_to_output(&mut self, output: &Output) {
if let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
{
let new_idx = monitors
.iter()
.position(|mon| &mon.output == output)
.unwrap();
let current = &mut monitors[*active_monitor_idx];
let ws = current.active_workspace();
2024-11-29 21:11:02 +03:00
if ws.floating_is_active() {
self.move_to_output(None, output, None);
return;
}
2024-11-30 09:18:33 +03:00
let Some(column) = ws.remove_active_column() else {
2024-01-15 10:36:59 +04:00
return;
2024-11-30 09:18:33 +03:00
};
2024-01-15 10:36:59 +04:00
let workspace_idx = monitors[new_idx].active_workspace_idx;
self.add_column_by_idx(new_idx, workspace_idx, column, true);
}
}
pub fn move_workspace_to_output(&mut self, output: &Output) {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
return;
};
let current = &mut monitors[*active_monitor_idx];
if current.active_workspace_idx == current.workspaces.len() - 1 {
// Insert a new empty workspace.
current.add_workspace_bottom();
}
if current.options.empty_workspace_above_first && current.active_workspace_idx == 0 {
current.add_workspace_top();
}
let mut ws = current.workspaces.remove(current.active_workspace_idx);
current.active_workspace_idx = current.active_workspace_idx.saturating_sub(1);
current.workspace_switch = None;
current.clean_up_workspaces();
ws.set_output(Some(output.clone()));
ws.original_output = OutputId::new(output);
let target_idx = monitors
.iter()
.position(|mon| &mon.output == output)
.unwrap();
let target = &mut monitors[target_idx];
2024-03-19 14:27:52 +00:00
target.previous_workspace_id = Some(target.workspaces[target.active_workspace_idx].id());
if target.options.empty_workspace_above_first && target.workspaces.len() == 1 {
// Insert a new empty workspace on top to prepare for insertion of new workspce.
target.add_workspace_top();
}
// Insert the workspace after the currently active one. Unless the currently active one is
// the last empty workspace, then insert before.
let target_ws_idx = min(target.active_workspace_idx + 1, target.workspaces.len() - 1);
target.workspaces.insert(target_ws_idx, ws);
target.active_workspace_idx = target_ws_idx;
target.workspace_switch = None;
target.clean_up_workspaces();
*active_monitor_idx = target_idx;
}
pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().id() == window {
return;
}
}
match &mut self.monitor_set {
2023-08-16 09:08:10 +04:00
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
if ws.has_window(window) {
ws.set_fullscreen(window, is_fullscreen);
return;
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
2023-08-16 09:08:10 +04:00
for ws in workspaces {
if ws.has_window(window) {
ws.set_fullscreen(window, is_fullscreen);
return;
}
}
}
}
}
pub fn toggle_fullscreen(&mut self, window: &W::Id) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().id() == window {
return;
}
}
match &mut self.monitor_set {
2023-08-16 09:08:10 +04:00
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
if ws.has_window(window) {
ws.toggle_fullscreen(window);
return;
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
2023-08-16 09:08:10 +04:00
for ws in workspaces {
if ws.has_window(window) {
ws.toggle_fullscreen(window);
return;
}
}
}
}
}
2024-06-19 21:54:46 +03:00
pub fn workspace_switch_gesture_begin(&mut self, output: &Output, is_touchpad: bool) {
let monitors = match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => monitors,
MonitorSet::NoOutputs { .. } => unreachable!(),
};
for monitor in monitors {
// Cancel the gesture on other outputs.
if &monitor.output != output {
2024-06-19 21:54:46 +03:00
monitor.workspace_switch_gesture_end(true, None);
continue;
}
2024-06-19 21:54:46 +03:00
monitor.workspace_switch_gesture_begin(is_touchpad);
}
}
2024-03-02 14:33:22 +04:00
pub fn workspace_switch_gesture_update(
&mut self,
delta_y: f64,
timestamp: Duration,
2024-06-19 21:54:46 +03:00
is_touchpad: bool,
2024-03-02 14:33:22 +04:00
) -> Option<Option<Output>> {
let monitors = match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => monitors,
MonitorSet::NoOutputs { .. } => return None,
};
for monitor in monitors {
2024-06-19 21:54:46 +03:00
if let Some(refresh) =
monitor.workspace_switch_gesture_update(delta_y, timestamp, is_touchpad)
{
if refresh {
return Some(Some(monitor.output.clone()));
} else {
return Some(None);
}
}
}
None
}
2024-06-19 21:54:46 +03:00
pub fn workspace_switch_gesture_end(
&mut self,
cancelled: bool,
is_touchpad: Option<bool>,
) -> Option<Output> {
let monitors = match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => monitors,
MonitorSet::NoOutputs { .. } => return None,
};
for monitor in monitors {
2024-06-19 21:54:46 +03:00
if monitor.workspace_switch_gesture_end(cancelled, is_touchpad) {
return Some(monitor.output.clone());
}
}
None
}
2023-10-14 20:42:10 +04:00
2024-05-11 14:01:48 +04:00
pub fn view_offset_gesture_begin(&mut self, output: &Output, is_touchpad: bool) {
2024-02-29 08:56:20 +04:00
let monitors = match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => monitors,
MonitorSet::NoOutputs { .. } => unreachable!(),
};
for monitor in monitors {
for (idx, ws) in monitor.workspaces.iter_mut().enumerate() {
// Cancel the gesture on other workspaces.
if &monitor.output != output || idx != monitor.active_workspace_idx {
2024-05-11 14:01:48 +04:00
ws.view_offset_gesture_end(true, None);
2024-02-29 08:56:20 +04:00
continue;
}
2024-05-11 14:01:48 +04:00
ws.view_offset_gesture_begin(is_touchpad);
2024-02-29 08:56:20 +04:00
}
}
}
pub fn view_offset_gesture_update(
&mut self,
delta_x: f64,
timestamp: Duration,
2024-05-11 14:01:48 +04:00
is_touchpad: bool,
) -> Option<Option<Output>> {
2024-02-29 08:56:20 +04:00
let monitors = match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => monitors,
MonitorSet::NoOutputs { .. } => return None,
};
for monitor in monitors {
for ws in &mut monitor.workspaces {
2024-05-11 14:01:48 +04:00
if let Some(refresh) =
ws.view_offset_gesture_update(delta_x, timestamp, is_touchpad)
{
2024-02-29 08:56:20 +04:00
if refresh {
return Some(Some(monitor.output.clone()));
} else {
return Some(None);
}
}
}
}
None
}
2024-05-11 14:01:48 +04:00
pub fn view_offset_gesture_end(
&mut self,
cancelled: bool,
is_touchpad: Option<bool>,
) -> Option<Output> {
2024-02-29 08:56:20 +04:00
let monitors = match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => monitors,
MonitorSet::NoOutputs { .. } => return None,
};
for monitor in monitors {
for ws in &mut monitor.workspaces {
2024-05-11 14:01:48 +04:00
if ws.view_offset_gesture_end(cancelled, is_touchpad) {
2024-02-29 08:56:20 +04:00
return Some(monitor.output.clone());
}
}
}
None
}
2024-07-15 15:51:48 +02:00
pub fn interactive_move_begin(
&mut self,
window_id: W::Id,
output: &Output,
start_pos_within_output: Point<f64, Logical>,
) -> bool {
if self.interactive_move.is_some() {
return false;
}
let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else {
return false;
};
let Some((mon, (ws, ws_offset))) = monitors.iter().find_map(|mon| {
mon.workspaces_with_render_positions()
.find(|(ws, _)| ws.has_window(&window_id))
.map(|rv| (mon, rv))
}) else {
return false;
};
if mon.output() != output {
return false;
}
let (tile, tile_offset) = ws
.tiles_with_render_positions()
.find(|(tile, _)| tile.window().id() == &window_id)
.unwrap();
let window_offset = tile.window_loc();
let tile_pos = ws_offset + tile_offset;
let pointer_offset_within_window = start_pos_within_output - tile_pos - window_offset;
let window_size = tile.window_size();
let pointer_ratio_within_window = (
f64::clamp(pointer_offset_within_window.x / window_size.w, 0., 1.),
f64::clamp(pointer_offset_within_window.y / window_size.h, 0., 1.),
);
self.interactive_move = Some(InteractiveMoveState::Starting {
window_id,
pointer_delta: Point::from((0., 0.)),
pointer_ratio_within_window,
});
true
}
pub fn interactive_move_update(
&mut self,
window: &W::Id,
delta: Point<f64, Logical>,
output: Output,
pointer_pos_within_output: Point<f64, Logical>,
) -> bool {
let Some(state) = self.interactive_move.take() else {
return false;
};
match state {
InteractiveMoveState::Starting {
window_id,
mut pointer_delta,
pointer_ratio_within_window,
} => {
if window_id != *window {
self.interactive_move = Some(InteractiveMoveState::Starting {
window_id,
pointer_delta,
pointer_ratio_within_window,
});
return false;
}
pointer_delta += delta;
let (cx, cy) = (pointer_delta.x, pointer_delta.y);
let sq_dist = cx * cx + cy * cy;
let factor = RubberBand {
stiffness: 1.0,
limit: 0.5,
}
.band(sq_dist / INTERACTIVE_MOVE_START_THRESHOLD);
2024-11-29 21:11:02 +03:00
let (is_floating, tile) = self
2024-07-15 15:51:48 +02:00
.workspaces_mut()
2024-11-29 21:11:02 +03:00
.find(|ws| ws.has_window(&window_id))
.map(|ws| {
(
ws.is_floating(&window_id),
ws.tiles_mut()
.find(|tile| *tile.window().id() == window_id)
.unwrap(),
)
})
2024-07-15 15:51:48 +02:00
.unwrap();
tile.interactive_move_offset = pointer_delta.upscale(factor);
// Put it back to be able to easily return.
self.interactive_move = Some(InteractiveMoveState::Starting {
window_id: window_id.clone(),
pointer_delta,
pointer_ratio_within_window,
});
2024-11-29 21:11:02 +03:00
if !is_floating && sq_dist < INTERACTIVE_MOVE_START_THRESHOLD {
2024-07-15 15:51:48 +02:00
return true;
}
// If the pointer is currently on the window's own output, then we can animate the
// window movement from its current (rubberbanded and possibly moved away) position
// to the pointer. Otherwise, we just teleport it as the layout code is not aware
// of monitor positions.
//
2024-11-29 21:11:02 +03:00
// FIXME: when and if the layout code knows about monitor positions, this will be
// potentially animatable.
2024-07-15 15:51:48 +02:00
let mut tile_pos = None;
if let MonitorSet::Normal { monitors, .. } = &self.monitor_set {
if let Some((mon, (ws, ws_offset))) = monitors.iter().find_map(|mon| {
mon.workspaces_with_render_positions()
.find(|(ws, _)| ws.has_window(window))
.map(|rv| (mon, rv))
}) {
if mon.output() == &output {
let (_, tile_offset) = ws
.tiles_with_render_positions()
.find(|(tile, _)| tile.window().id() == window)
.unwrap();
tile_pos = Some(ws_offset + tile_offset);
}
}
}
let RemovedTile {
mut tile,
width,
is_full_width,
is_floating,
2024-07-15 15:51:48 +02:00
} = self.remove_window(window, Transaction::new()).unwrap();
tile.stop_move_animations();
tile.interactive_move_offset = Point::from((0., 0.));
tile.window().output_enter(&output);
tile.window().set_preferred_scale_transform(
output.current_scale(),
output.current_transform(),
);
let scale = output.current_scale().fractional_scale();
tile.update_config(
scale,
Rc::new(Options::clone(&self.options).adjusted_for_scale(scale)),
);
// Unfullscreen and let the window pick a natural size.
//
2024-11-29 21:11:02 +03:00
// TODO
2024-07-15 15:51:48 +02:00
// When we have floating, we will want to always send a (0, 0) size here, not just
// to unfullscreen. However, when implementing that, remember to check how GTK
// tiled window size restoration works. It seems to remember *some* last size with
// prefer-no-csd, and occasionally that last size can become the full-width size
// rather than a smaller size, which is annoying. Need to see if niri can use some
// heuristics to make this case behave better.
let win = tile.window_mut();
if win.is_pending_fullscreen() {
let mut size = Size::from((0, 0));
// Make sure fixed-size through window rules keeps working.
let min_size = win.min_size();
let max_size = win.max_size();
if min_size.w == max_size.w {
size.w = min_size.w;
}
if min_size.h == max_size.h {
size.h = min_size.h;
}
win.request_size(size, true, None);
2024-07-15 15:51:48 +02:00
}
let mut data = InteractiveMoveData {
tile,
output,
pointer_pos_within_output,
width,
is_full_width,
is_floating,
2024-07-15 15:51:48 +02:00
pointer_ratio_within_window,
};
if let Some(tile_pos) = tile_pos {
let new_tile_pos = data.tile_render_location();
data.tile.animate_move_from(tile_pos - new_tile_pos);
}
self.interactive_move = Some(InteractiveMoveState::Moving(data));
}
InteractiveMoveState::Moving(mut move_) => {
if window != move_.tile.window().id() {
self.interactive_move = Some(InteractiveMoveState::Moving(move_));
return false;
}
if output != move_.output {
move_.tile.window().output_leave(&move_.output);
move_.tile.window().output_enter(&output);
move_.tile.window().set_preferred_scale_transform(
output.current_scale(),
output.current_transform(),
);
let scale = output.current_scale().fractional_scale();
move_.tile.update_config(
scale,
Rc::new(Options::clone(&self.options).adjusted_for_scale(scale)),
);
move_.output = output.clone();
self.focus_output(&output);
}
move_.pointer_pos_within_output = pointer_pos_within_output;
self.interactive_move = Some(InteractiveMoveState::Moving(move_));
}
}
true
}
pub fn interactive_move_end(&mut self, window: &W::Id) {
let Some(move_) = &self.interactive_move else {
return;
};
let move_ = match move_ {
InteractiveMoveState::Starting { window_id, .. } => {
if window_id != window {
return;
}
let Some(InteractiveMoveState::Starting { window_id, .. }) =
self.interactive_move.take()
else {
unreachable!()
};
let tile = self
.workspaces_mut()
.flat_map(|ws| ws.tiles_mut())
.find(|tile| *tile.window().id() == window_id)
.unwrap();
let offset = tile.interactive_move_offset;
tile.interactive_move_offset = Point::from((0., 0.));
tile.animate_move_from(offset);
return;
}
InteractiveMoveState::Moving(move_) => move_,
};
if window != move_.tile.window().id() {
return;
}
let Some(InteractiveMoveState::Moving(move_)) = self.interactive_move.take() else {
unreachable!()
};
match &mut self.monitor_set {
MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} => {
let (mon, ws_idx, position, offset) = if let Some(mon) =
monitors.iter_mut().find(|mon| mon.output == move_.output)
{
let (ws, offset) = mon
.workspace_under(move_.pointer_pos_within_output)
// If the pointer is somehow outside the move output and a workspace switch
// is in progress, this won't necessarily do the expected thing, but also
// that is not really supposed to happen so eh?
.unwrap_or_else(|| mon.workspaces_with_render_positions().next().unwrap());
let ws_id = ws.id();
let ws_idx = mon
.workspaces
.iter_mut()
.position(|ws| ws.id() == ws_id)
.unwrap();
let position = if move_.is_floating {
InsertPosition::Floating
} else {
let ws = &mut mon.workspaces[ws_idx];
ws.get_insert_position(move_.pointer_pos_within_output - offset)
};
2024-07-15 15:51:48 +02:00
(mon, ws_idx, position, offset)
} else {
let mon = &mut monitors[*active_monitor_idx];
// No point in trying to use the pointer position on the wrong output.
let (ws, offset) = mon.workspaces_with_render_positions().next().unwrap();
let position = if move_.is_floating {
InsertPosition::Floating
} else {
ws.get_insert_position(Point::from((0., 0.)))
};
let ws_id = ws.id();
let ws_idx = mon
.workspaces
.iter_mut()
.position(|ws| ws.id() == ws_id)
.unwrap();
2024-07-15 15:51:48 +02:00
(mon, ws_idx, position, offset)
};
let win_id = move_.tile.window().id().clone();
let window_render_loc = move_.tile_render_location() + move_.tile.window_loc();
match position {
InsertPosition::NewColumn(column_idx) => {
mon.add_tile(
ws_idx,
Some(column_idx),
move_.tile,
true,
move_.width,
move_.is_full_width,
);
}
InsertPosition::InColumn(column_idx, tile_idx) => {
mon.add_tile_to_column(
ws_idx,
column_idx,
Some(tile_idx),
move_.tile,
true,
);
}
2024-11-29 21:11:02 +03:00
InsertPosition::Floating => {
let pos = move_.tile_render_location() - offset;
mon.add_floating_tile(ws_idx, move_.tile, Some(pos), true);
}
2024-07-15 15:51:48 +02:00
}
// needed because empty_workspace_above_first could have modified the idx
let ws_idx = mon.active_workspace_idx();
2024-07-15 15:51:48 +02:00
let ws = &mut mon.workspaces[ws_idx];
let (tile, tile_render_loc) = ws
.tiles_with_render_positions_mut(false)
.find(|(tile, _)| tile.window().id() == &win_id)
.unwrap();
let new_window_render_loc = offset + tile_render_loc + tile.window_loc();
tile.animate_move_from(window_render_loc - new_window_render_loc);
}
MonitorSet::NoOutputs { workspaces, .. } => {
let ws = if let Some(ws) = workspaces.get_mut(0) {
ws
} else {
workspaces.push(Workspace::new_no_outputs(
self.clock.clone(),
self.options.clone(),
));
2024-07-15 15:51:48 +02:00
&mut workspaces[0]
};
// No point in trying to use the pointer position without outputs.
2024-11-30 09:18:33 +03:00
ws.add_tile(None, move_.tile, true, move_.width, move_.is_full_width);
2024-07-15 15:51:48 +02:00
}
}
}
2024-05-10 16:58:53 +04:00
pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
if ws.has_window(&window) {
return ws.interactive_resize_begin(window, edges);
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
if ws.has_window(&window) {
return ws.interactive_resize_begin(window, edges);
}
}
}
}
false
}
pub fn interactive_resize_update(
&mut self,
window: &W::Id,
delta: Point<f64, Logical>,
) -> bool {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().id() == window {
return false;
}
}
2024-05-10 16:58:53 +04:00
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
if ws.has_window(window) {
return ws.interactive_resize_update(window, delta);
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
if ws.has_window(window) {
return ws.interactive_resize_update(window, delta);
}
}
}
}
false
}
pub fn interactive_resize_end(&mut self, window: &W::Id) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().id() == window {
return;
}
}
2024-05-10 16:58:53 +04:00
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
if ws.has_window(window) {
ws.interactive_resize_end(Some(window));
return;
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
if ws.has_window(window) {
ws.interactive_resize_end(Some(window));
return;
}
}
}
}
}
2023-10-14 20:42:10 +04:00
pub fn move_workspace_down(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.move_workspace_down();
}
pub fn move_workspace_up(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.move_workspace_up();
}
2024-02-07 11:32:02 +04:00
pub fn start_open_animation_for_window(&mut self, window: &W::Id) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().id() == window {
return;
}
}
2024-11-30 09:18:33 +03:00
for ws in self.workspaces_mut() {
for tile in ws.tiles_mut() {
if tile.window().id() == window {
tile.start_open_animation();
return;
2024-02-07 11:32:02 +04:00
}
}
}
}
2024-05-01 19:00:11 +04:00
pub fn store_unmap_snapshot(&mut self, renderer: &mut GlesRenderer, window: &W::Id) {
let _span = tracy_client::span!("Layout::store_unmap_snapshot");
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if move_.tile.window().id() == window {
let scale = Scale::from(move_.output.current_scale().fractional_scale());
move_.tile.store_unmap_snapshot_if_empty(renderer, scale);
return;
}
}
2024-05-01 19:00:11 +04:00
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
if ws.has_window(window) {
ws.store_unmap_snapshot_if_empty(renderer, window);
return;
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
if ws.has_window(window) {
ws.store_unmap_snapshot_if_empty(renderer, window);
return;
}
}
}
}
}
pub fn clear_unmap_snapshot(&mut self, window: &W::Id) {
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if move_.tile.window().id() == window {
let _ = move_.tile.take_unmap_snapshot();
return;
}
}
2024-05-01 19:00:11 +04:00
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
if ws.has_window(window) {
ws.clear_unmap_snapshot(window);
return;
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
if ws.has_window(window) {
ws.clear_unmap_snapshot(window);
return;
}
}
}
}
}
2024-04-09 22:37:10 +04:00
pub fn start_close_animation_for_window(
&mut self,
renderer: &mut GlesRenderer,
window: &W::Id,
2024-08-23 15:41:06 +03:00
blocker: TransactionBlocker,
2024-04-09 22:37:10 +04:00
) {
let _span = tracy_client::span!("Layout::start_close_animation_for_window");
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if move_.tile.window().id() == window {
let Some(snapshot) = move_.tile.take_unmap_snapshot() else {
return;
};
let tile_pos = move_.tile_render_location();
let tile_size = move_.tile.tile_size();
let output = move_.output.clone();
let pointer_pos_within_output = move_.pointer_pos_within_output;
let Some(mon) = self.monitor_for_output_mut(&output) else {
return;
};
let Some((ws, offset)) = mon.workspace_under(pointer_pos_within_output) else {
return;
};
let ws_id = ws.id();
let ws = mon
.workspaces
.iter_mut()
.find(|ws| ws.id() == ws_id)
.unwrap();
2024-11-30 09:18:33 +03:00
let tile_pos = tile_pos - offset;
2024-07-15 15:51:48 +02:00
ws.start_close_animation_for_tile(renderer, snapshot, tile_size, tile_pos, blocker);
return;
}
}
2024-04-09 22:37:10 +04:00
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
if ws.has_window(window) {
2024-08-23 15:41:06 +03:00
ws.start_close_animation_for_window(renderer, window, blocker);
2024-04-09 22:37:10 +04:00
return;
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
if ws.has_window(window) {
2024-08-23 15:41:06 +03:00
ws.start_close_animation_for_window(renderer, window, blocker);
2024-04-09 22:37:10 +04:00
return;
}
}
}
}
}
2024-07-15 15:51:48 +02:00
pub fn render_floating_for_output<R: NiriRenderer>(
&self,
renderer: &mut R,
output: &Output,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> {
if self.update_render_elements_time != self.clock.now() {
error!("clock moved between updating render elements and rendering");
}
2024-07-15 15:51:48 +02:00
let mut rv = None;
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if &move_.output == output {
let scale = Scale::from(move_.output.current_scale().fractional_scale());
let location = move_.tile_render_location();
rv = Some(move_.tile.render(renderer, location, scale, true, target));
}
}
rv.into_iter().flatten()
}
pub fn refresh(&mut self, is_active: bool) {
2024-03-19 13:52:08 +04:00
let _span = tracy_client::span!("Layout::refresh");
2023-08-14 14:48:10 +04:00
self.is_active = is_active;
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
let win = move_.tile.window_mut();
win.set_active_in_column(true);
win.set_activated(true);
win.set_interactive_resize(None);
win.set_bounds(output_size(&move_.output).to_i32_round());
win.send_pending_configure();
win.refresh();
}
2024-02-29 08:56:20 +04:00
match &mut self.monitor_set {
2023-12-28 09:09:28 +04:00
MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} => {
2024-02-29 08:56:20 +04:00
for (idx, mon) in monitors.iter_mut().enumerate() {
let is_active = self.is_active
&& idx == *active_monitor_idx
&& !matches!(self.interactive_move, Some(InteractiveMoveState::Moving(_)));
2024-02-29 08:56:20 +04:00
for (ws_idx, ws) in mon.workspaces.iter_mut().enumerate() {
2023-12-28 09:09:28 +04:00
ws.refresh(is_active);
2024-02-29 08:56:20 +04:00
// Cancel the view offset gesture after workspace switches, moves, etc.
if ws_idx != mon.active_workspace_idx {
2024-05-11 14:01:48 +04:00
ws.view_offset_gesture_end(false, None);
2024-02-29 08:56:20 +04:00
}
2023-08-14 14:48:10 +04:00
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
2023-08-14 14:48:10 +04:00
for ws in workspaces {
2023-12-28 09:09:28 +04:00
ws.refresh(false);
2024-05-11 14:01:48 +04:00
ws.view_offset_gesture_end(false, None);
2023-08-14 14:48:10 +04:00
}
}
}
}
2024-05-16 14:30:52 -07:00
2024-06-20 12:04:10 +03:00
pub fn workspaces(
&self,
) -> impl Iterator<Item = (Option<&Monitor<W>>, usize, &Workspace<W>)> + '_ {
let iter_normal;
let iter_no_outputs;
2024-05-16 14:30:52 -07:00
match &self.monitor_set {
2024-06-20 12:04:10 +03:00
MonitorSet::Normal { monitors, .. } => {
let it = monitors.iter().flat_map(|mon| {
mon.workspaces
.iter()
.enumerate()
.map(move |(idx, ws)| (Some(mon), idx, ws))
});
2024-05-16 14:30:52 -07:00
2024-06-20 12:04:10 +03:00
iter_normal = Some(it);
iter_no_outputs = None;
}
MonitorSet::NoOutputs { workspaces } => {
let it = workspaces
.iter()
.enumerate()
.map(|(idx, ws)| (None, idx, ws));
iter_normal = None;
iter_no_outputs = Some(it);
2024-05-16 14:30:52 -07:00
}
}
2024-06-20 12:04:10 +03:00
let iter_normal = iter_normal.into_iter().flatten();
let iter_no_outputs = iter_no_outputs.into_iter().flatten();
iter_normal.chain(iter_no_outputs)
2024-05-16 14:30:52 -07:00
}
pub fn workspaces_mut(&mut self) -> impl Iterator<Item = &mut Workspace<W>> + '_ {
let iter_normal;
let iter_no_outputs;
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
let it = monitors
.iter_mut()
.flat_map(|mon| mon.workspaces.iter_mut());
iter_normal = Some(it);
iter_no_outputs = None;
}
MonitorSet::NoOutputs { workspaces } => {
let it = workspaces.iter_mut();
iter_normal = None;
iter_no_outputs = Some(it);
}
}
let iter_normal = iter_normal.into_iter().flatten();
let iter_no_outputs = iter_no_outputs.into_iter().flatten();
iter_normal.chain(iter_no_outputs)
}
pub fn windows(&self) -> impl Iterator<Item = (Option<&Monitor<W>>, &W)> {
2024-07-15 15:51:48 +02:00
let moving_window = self
.interactive_move
.as_ref()
.and_then(|x| x.moving())
.map(|move_| (self.monitor_for_output(&move_.output), move_.tile.window()))
.into_iter();
let rest = self
.workspaces()
.flat_map(|(mon, _, ws)| ws.windows().map(move |win| (mon, win)));
moving_window.chain(rest)
}
pub fn has_window(&self, window: &W::Id) -> bool {
self.windows().any(|(_, win)| win.id() == window)
}
fn resolve_default_width(&self, window: &W, width: Option<ColumnWidth>) -> ColumnWidth {
let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(f64::from(window.size().w)));
if let ColumnWidth::Fixed(w) = &mut width {
let rules = window.rules();
let border_config = rules.border.resolve_against(self.options.border);
if !border_config.off {
*w += border_config.width.0 * 2.;
}
}
width
}
}
impl<W: LayoutElement> Default for MonitorSet<W> {
fn default() -> Self {
Self::NoOutputs { workspaces: vec![] }
}
}
2023-09-12 19:44:17 +04:00
#[cfg(test)]
mod tests {
use std::cell::Cell;
use niri_config::{FloatOrInt, OutputName, WorkspaceName};
2023-09-27 13:35:02 +04:00
use proptest::prelude::*;
use proptest_derive::Arbitrary;
2023-09-12 19:44:17 +04:00
use smithay::output::{Mode, PhysicalProperties, Subpixel};
2024-03-19 14:41:17 +04:00
use smithay::utils::Rectangle;
2023-09-12 19:44:17 +04:00
use super::*;
impl<W: LayoutElement> Default for Layout<W> {
fn default() -> Self {
Self::with_options(Clock::with_time(Duration::ZERO), Default::default())
}
}
2023-09-12 19:44:17 +04:00
#[derive(Debug)]
struct TestWindowInner {
id: usize,
bbox: Cell<Rectangle<i32, Logical>>,
2023-09-28 08:59:45 +04:00
initial_bbox: Rectangle<i32, Logical>,
requested_size: Cell<Option<Size<i32, Logical>>>,
2023-12-29 11:42:54 +04:00
min_size: Size<i32, Logical>,
max_size: Size<i32, Logical>,
pending_fullscreen: Cell<bool>,
pending_activated: Cell<bool>,
2023-09-12 19:44:17 +04:00
}
#[derive(Debug, Clone)]
struct TestWindow(Rc<TestWindowInner>);
impl TestWindow {
2023-12-29 11:42:54 +04:00
fn new(
id: usize,
bbox: Rectangle<i32, Logical>,
min_size: Size<i32, Logical>,
max_size: Size<i32, Logical>,
) -> Self {
2023-09-12 19:44:17 +04:00
Self(Rc::new(TestWindowInner {
id,
bbox: Cell::new(bbox),
2023-09-28 08:59:45 +04:00
initial_bbox: bbox,
requested_size: Cell::new(None),
2023-12-29 11:42:54 +04:00
min_size,
max_size,
pending_fullscreen: Cell::new(false),
pending_activated: Cell::new(false),
2023-09-12 19:44:17 +04:00
}))
}
2023-09-28 08:59:45 +04:00
fn communicate(&self) -> bool {
if let Some(size) = self.0.requested_size.get() {
2023-09-28 08:59:45 +04:00
assert!(size.w >= 0);
assert!(size.h >= 0);
let mut new_bbox = self.0.initial_bbox;
if size.w != 0 {
new_bbox.size.w = size.w;
}
if size.h != 0 {
new_bbox.size.h = size.h;
}
if self.0.bbox.get() != new_bbox {
self.0.bbox.set(new_bbox);
return true;
}
}
false
}
2023-09-12 19:44:17 +04:00
}
impl LayoutElement for TestWindow {
type Id = usize;
fn id(&self) -> &Self::Id {
&self.0.id
2023-09-12 19:44:17 +04:00
}
fn size(&self) -> Size<i32, Logical> {
self.0.bbox.get().size
}
fn buf_loc(&self) -> Point<i32, Logical> {
(0, 0).into()
2023-09-12 19:44:17 +04:00
}
fn is_in_input_region(&self, _point: Point<f64, Logical>) -> bool {
2023-09-12 19:44:17 +04:00
false
}
fn render<R: NiriRenderer>(
&self,
_renderer: &mut R,
2024-06-17 09:16:28 +03:00
_location: Point<f64, Logical>,
_scale: Scale<f64>,
2024-03-24 08:30:26 +04:00
_alpha: f32,
_target: RenderTarget,
) -> SplitElements<LayoutElementRenderElement<R>> {
SplitElements::default()
}
2024-08-22 14:44:11 +03:00
fn request_size(
&mut self,
size: Size<i32, Logical>,
_animate: bool,
_transaction: Option<Transaction>,
) {
2023-09-28 08:59:45 +04:00
self.0.requested_size.set(Some(size));
self.0.pending_fullscreen.set(false);
2023-09-28 08:59:45 +04:00
}
2023-09-12 19:44:17 +04:00
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
self.0.pending_fullscreen.set(true);
}
2023-09-12 19:44:17 +04:00
fn min_size(&self) -> Size<i32, Logical> {
2023-12-29 11:42:54 +04:00
self.0.min_size
2023-09-12 19:44:17 +04:00
}
fn max_size(&self) -> Size<i32, Logical> {
2023-12-29 11:42:54 +04:00
self.0.max_size
2023-09-12 19:44:17 +04:00
}
fn is_wl_surface(&self, _wl_surface: &WlSurface) -> bool {
false
}
2023-10-11 14:32:29 +04:00
2024-05-29 13:32:11 +03:00
fn set_preferred_scale_transform(&self, _scale: output::Scale, _transform: Transform) {}
2023-10-26 00:15:46 +04:00
2023-10-11 14:32:29 +04:00
fn has_ssd(&self) -> bool {
false
}
fn output_enter(&self, _output: &Output) {}
fn output_leave(&self, _output: &Output) {}
2024-02-07 11:32:02 +04:00
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
fn set_activated(&mut self, active: bool) {
self.0.pending_activated.set(active);
}
2024-03-19 13:52:08 +04:00
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
2024-08-22 14:36:47 +03:00
fn configure_intent(&self) -> ConfigureIntent {
ConfigureIntent::CanSend
}
2024-04-13 11:07:23 +04:00
fn send_pending_configure(&mut self) {}
2024-03-19 13:52:08 +04:00
2024-04-22 22:51:52 +02:00
fn set_active_in_column(&mut self, _active: bool) {}
fn is_fullscreen(&self) -> bool {
false
}
fn is_pending_fullscreen(&self) -> bool {
self.0.pending_fullscreen.get()
}
2024-03-19 13:52:08 +04:00
fn requested_size(&self) -> Option<Size<i32, Logical>> {
self.0.requested_size.get()
}
2024-03-19 13:52:08 +04:00
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
&EMPTY
}
2024-04-13 11:07:23 +04:00
2024-04-13 14:16:07 +04:00
fn animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot> {
2024-04-13 11:07:23 +04:00
None
}
2024-04-13 14:16:07 +04:00
fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> {
2024-04-13 11:07:23 +04:00
None
}
2024-05-10 16:58:53 +04:00
fn set_interactive_resize(&mut self, _data: Option<InteractiveResizeData>) {}
fn cancel_interactive_resize(&mut self) {}
2024-05-11 08:26:49 +04:00
fn update_interactive_resize(&mut self, _serial: Serial) {}
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
2024-05-10 16:58:53 +04:00
None
}
2023-09-12 19:44:17 +04:00
}
2023-09-27 13:35:02 +04:00
fn arbitrary_bbox() -> impl Strategy<Value = Rectangle<i32, Logical>> {
any::<(i16, i16, u16, u16)>().prop_map(|(x, y, w, h)| {
let loc: Point<i32, _> = Point::from((x.into(), y.into()));
2024-07-15 15:51:48 +02:00
let size: Size<i32, _> = Size::from((w.max(1).into(), h.max(1).into()));
2023-09-27 13:35:02 +04:00
Rectangle::from_loc_and_size(loc, size)
})
}
2023-10-03 11:38:42 +04:00
fn arbitrary_size_change() -> impl Strategy<Value = SizeChange> {
prop_oneof![
(0..).prop_map(SizeChange::SetFixed),
(0f64..).prop_map(SizeChange::SetProportion),
any::<i32>().prop_map(SizeChange::AdjustFixed),
any::<f64>().prop_map(SizeChange::AdjustProportion),
2024-11-29 21:11:02 +03:00
// Interactive resize can have negative values here.
Just(SizeChange::SetFixed(-100)),
2023-10-03 11:38:42 +04:00
]
}
2023-12-29 11:42:54 +04:00
fn arbitrary_min_max() -> impl Strategy<Value = (i32, i32)> {
prop_oneof![
Just((0, 0)),
(1..65536).prop_map(|n| (n, n)),
(1..65536).prop_map(|min| (min, 0)),
(1..).prop_map(|max| (0, max)),
(1..65536, 1..).prop_map(|(min, max): (i32, i32)| (min, max.max(min))),
]
}
fn arbitrary_min_max_size() -> impl Strategy<Value = (Size<i32, Logical>, Size<i32, Logical>)> {
2024-11-29 21:11:02 +03:00
prop_oneof![
5 => (arbitrary_min_max(), arbitrary_min_max()).prop_map(
|((min_w, max_w), (min_h, max_h))| {
let min_size = Size::from((min_w, min_h));
let max_size = Size::from((max_w, max_h));
(min_size, max_size)
},
),
1 => arbitrary_min_max().prop_map(|(w, h)| {
let size = Size::from((w, h));
(size, size)
}),
]
2023-12-29 11:42:54 +04:00
}
2024-02-29 08:56:20 +04:00
fn arbitrary_view_offset_gesture_delta() -> impl Strategy<Value = f64> {
prop_oneof![(-10f64..10f64), (-50000f64..50000f64),]
}
2024-05-10 16:58:53 +04:00
fn arbitrary_resize_edge() -> impl Strategy<Value = ResizeEdge> {
prop_oneof![
Just(ResizeEdge::RIGHT),
Just(ResizeEdge::BOTTOM),
Just(ResizeEdge::LEFT),
Just(ResizeEdge::TOP),
Just(ResizeEdge::BOTTOM_RIGHT),
Just(ResizeEdge::BOTTOM_LEFT),
Just(ResizeEdge::TOP_RIGHT),
Just(ResizeEdge::TOP_LEFT),
Just(ResizeEdge::empty()),
]
}
2024-06-17 09:16:28 +03:00
fn arbitrary_scale() -> impl Strategy<Value = f64> {
prop_oneof![Just(1.), Just(1.5), Just(2.),]
}
fn arbitrary_msec_delta() -> impl Strategy<Value = i32> {
prop_oneof![
1 => Just(-1000),
2 => Just(-10),
1 => Just(0),
2 => Just(10),
6 => Just(1000),
]
}
2023-09-27 13:35:02 +04:00
#[derive(Debug, Clone, Copy, Arbitrary)]
2023-09-12 19:44:17 +04:00
enum Op {
2023-09-27 13:35:02 +04:00
AddOutput(#[proptest(strategy = "1..=5usize")] usize),
2024-06-17 09:16:28 +03:00
AddScaledOutput {
#[proptest(strategy = "1..=5usize")]
id: usize,
#[proptest(strategy = "arbitrary_scale()")]
scale: f64,
},
2023-09-27 13:35:02 +04:00
RemoveOutput(#[proptest(strategy = "1..=5usize")] usize),
FocusOutput(#[proptest(strategy = "1..=5usize")] usize),
2024-05-11 22:40:30 +02:00
AddNamedWorkspace {
#[proptest(strategy = "1..=5usize")]
ws_name: usize,
#[proptest(strategy = "prop::option::of(1..=5usize)")]
output_name: Option<usize>,
},
UnnameWorkspace {
#[proptest(strategy = "1..=5usize")]
ws_name: usize,
},
2023-09-12 19:44:17 +04:00
AddWindow {
2023-09-27 13:35:02 +04:00
#[proptest(strategy = "1..=5usize")]
2023-09-12 19:44:17 +04:00
id: usize,
2023-09-27 13:35:02 +04:00
#[proptest(strategy = "arbitrary_bbox()")]
2023-09-12 19:44:17 +04:00
bbox: Rectangle<i32, Logical>,
2023-12-29 11:42:54 +04:00
#[proptest(strategy = "arbitrary_min_max_size()")]
min_max_size: (Size<i32, Logical>, Size<i32, Logical>),
2023-09-12 19:44:17 +04:00
},
AddWindowRightOf {
#[proptest(strategy = "1..=5usize")]
id: usize,
#[proptest(strategy = "1..=5usize")]
right_of_id: usize,
#[proptest(strategy = "arbitrary_bbox()")]
bbox: Rectangle<i32, Logical>,
#[proptest(strategy = "arbitrary_min_max_size()")]
min_max_size: (Size<i32, Logical>, Size<i32, Logical>),
},
2024-05-11 22:40:30 +02:00
AddWindowToNamedWorkspace {
#[proptest(strategy = "1..=5usize")]
id: usize,
#[proptest(strategy = "1..=5usize")]
ws_name: usize,
#[proptest(strategy = "arbitrary_bbox()")]
bbox: Rectangle<i32, Logical>,
#[proptest(strategy = "arbitrary_min_max_size()")]
min_max_size: (Size<i32, Logical>, Size<i32, Logical>),
},
2023-09-27 13:35:02 +04:00
CloseWindow(#[proptest(strategy = "1..=5usize")] usize),
FullscreenWindow(#[proptest(strategy = "1..=5usize")] usize),
2024-06-04 19:44:33 +03:00
SetFullscreenWindow {
#[proptest(strategy = "1..=5usize")]
window: usize,
is_fullscreen: bool,
},
2023-09-12 19:44:17 +04:00
FocusColumnLeft,
FocusColumnRight,
2023-12-29 07:51:14 +04:00
FocusColumnFirst,
FocusColumnLast,
FocusColumnRightOrFirst,
FocusColumnLeftOrLast,
FocusWindowOrMonitorUp(#[proptest(strategy = "1..=2u8")] u8),
FocusWindowOrMonitorDown(#[proptest(strategy = "1..=2u8")] u8),
FocusColumnOrMonitorLeft(#[proptest(strategy = "1..=2u8")] u8),
FocusColumnOrMonitorRight(#[proptest(strategy = "1..=2u8")] u8),
FocusWindowDown,
FocusWindowUp,
FocusWindowDownOrColumnLeft,
FocusWindowDownOrColumnRight,
FocusWindowUpOrColumnLeft,
FocusWindowUpOrColumnRight,
FocusWindowOrWorkspaceDown,
FocusWindowOrWorkspaceUp,
2023-09-12 19:44:17 +04:00
MoveColumnLeft,
MoveColumnRight,
2023-12-29 08:01:02 +04:00
MoveColumnToFirst,
MoveColumnToLast,
MoveColumnLeftOrToMonitorLeft(#[proptest(strategy = "1..=2u8")] u8),
MoveColumnRightOrToMonitorRight(#[proptest(strategy = "1..=2u8")] u8),
MoveWindowDown,
MoveWindowUp,
MoveWindowDownOrToWorkspaceDown,
MoveWindowUpOrToWorkspaceUp,
ConsumeOrExpelWindowLeft {
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
id: Option<usize>,
},
ConsumeOrExpelWindowRight {
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
id: Option<usize>,
},
2023-09-12 19:44:17 +04:00
ConsumeWindowIntoColumn,
ExpelWindowFromColumn,
CenterColumn,
2023-09-12 19:44:17 +04:00
FocusWorkspaceDown,
FocusWorkspaceUp,
FocusWorkspace(#[proptest(strategy = "0..=4usize")] usize),
2024-03-19 14:27:52 +00:00
FocusWorkspaceAutoBackAndForth(#[proptest(strategy = "0..=4usize")] usize),
FocusWorkspacePrevious,
2023-09-12 19:44:17 +04:00
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceUp,
MoveWindowToWorkspace {
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
window_id: Option<usize>,
#[proptest(strategy = "0..=4usize")]
workspace_idx: usize,
},
2024-01-15 10:31:44 +04:00
MoveColumnToWorkspaceDown,
MoveColumnToWorkspaceUp,
MoveColumnToWorkspace(#[proptest(strategy = "0..=4usize")] usize),
2023-10-14 20:42:10 +04:00
MoveWorkspaceDown,
MoveWorkspaceUp,
MoveWindowToOutput {
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
window_id: Option<usize>,
#[proptest(strategy = "1..=5usize")]
output_id: usize,
#[proptest(strategy = "proptest::option::of(0..=4usize)")]
target_ws_idx: Option<usize>,
},
MoveColumnToOutput(#[proptest(strategy = "1..=5usize")] usize),
SwitchPresetColumnWidth,
2024-09-12 11:53:10 +03:00
SwitchPresetWindowHeight {
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
id: Option<usize>,
},
MaximizeColumn,
2023-10-03 11:38:42 +04:00
SetColumnWidth(#[proptest(strategy = "arbitrary_size_change()")] SizeChange),
SetWindowHeight {
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
id: Option<usize>,
#[proptest(strategy = "arbitrary_size_change()")]
change: SizeChange,
},
ResetWindowHeight {
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
id: Option<usize>,
},
2024-11-29 21:11:02 +03:00
ToggleWindowFloating {
#[proptest(strategy = "proptest::option::of(1..=5usize)")]
id: Option<usize>,
},
SwitchFocusFloatingTiling,
2023-09-28 08:59:45 +04:00
Communicate(#[proptest(strategy = "1..=5usize")] usize),
Refresh {
is_active: bool,
},
AdvanceAnimations {
#[proptest(strategy = "arbitrary_msec_delta()")]
msec_delta: i32,
},
MoveWorkspaceToOutput(#[proptest(strategy = "1..=5usize")] usize),
2024-02-29 08:56:20 +04:00
ViewOffsetGestureBegin {
#[proptest(strategy = "1..=5usize")]
output_idx: usize,
2024-05-11 14:01:48 +04:00
is_touchpad: bool,
2024-02-29 08:56:20 +04:00
},
ViewOffsetGestureUpdate {
#[proptest(strategy = "arbitrary_view_offset_gesture_delta()")]
delta: f64,
timestamp: Duration,
2024-05-11 14:01:48 +04:00
is_touchpad: bool,
},
ViewOffsetGestureEnd {
is_touchpad: Option<bool>,
2024-02-29 08:56:20 +04:00
},
WorkspaceSwitchGestureBegin {
#[proptest(strategy = "1..=5usize")]
output_idx: usize,
2024-06-19 21:54:46 +03:00
is_touchpad: bool,
},
WorkspaceSwitchGestureUpdate {
#[proptest(strategy = "-400f64..400f64")]
delta: f64,
2024-03-02 14:33:22 +04:00
timestamp: Duration,
2024-06-19 21:54:46 +03:00
is_touchpad: bool,
},
WorkspaceSwitchGestureEnd {
cancelled: bool,
2024-06-19 21:54:46 +03:00
is_touchpad: Option<bool>,
},
2024-07-15 15:51:48 +02:00
InteractiveMoveBegin {
#[proptest(strategy = "1..=5usize")]
window: usize,
#[proptest(strategy = "1..=5usize")]
output_idx: usize,
#[proptest(strategy = "-20000f64..20000f64")]
px: f64,
#[proptest(strategy = "-20000f64..20000f64")]
py: f64,
},
InteractiveMoveUpdate {
#[proptest(strategy = "1..=5usize")]
window: usize,
#[proptest(strategy = "-20000f64..20000f64")]
dx: f64,
#[proptest(strategy = "-20000f64..20000f64")]
dy: f64,
#[proptest(strategy = "1..=5usize")]
output_idx: usize,
#[proptest(strategy = "-20000f64..20000f64")]
px: f64,
#[proptest(strategy = "-20000f64..20000f64")]
py: f64,
},
InteractiveMoveEnd {
#[proptest(strategy = "1..=5usize")]
window: usize,
},
2024-05-10 16:58:53 +04:00
InteractiveResizeBegin {
#[proptest(strategy = "1..=5usize")]
window: usize,
#[proptest(strategy = "arbitrary_resize_edge()")]
edges: ResizeEdge,
},
InteractiveResizeUpdate {
#[proptest(strategy = "1..=5usize")]
window: usize,
#[proptest(strategy = "-20000f64..20000f64")]
dx: f64,
#[proptest(strategy = "-20000f64..20000f64")]
dy: f64,
},
InteractiveResizeEnd {
#[proptest(strategy = "1..=5usize")]
window: usize,
},
2023-09-12 19:44:17 +04:00
}
impl Op {
fn apply(self, layout: &mut Layout<TestWindow>) {
2023-09-12 19:44:17 +04:00
match self {
Op::AddOutput(id) => {
let name = format!("output{id}");
if layout.outputs().any(|o| o.name() == name) {
2023-09-12 19:44:17 +04:00
return;
}
let output = Output::new(
name.clone(),
2023-09-12 19:44:17 +04:00
PhysicalProperties {
size: Size::from((1280, 720)),
subpixel: Subpixel::Unknown,
make: String::new(),
model: String::new(),
},
);
output.change_current_state(
Some(Mode {
size: Size::from((1280, 720)),
2024-02-06 19:09:27 +04:00
refresh: 60000,
2023-09-12 19:44:17 +04:00
}),
None,
None,
None,
);
output.user_data().insert_if_missing(|| OutputName {
connector: name,
make: None,
model: None,
serial: None,
});
layout.add_output(output.clone());
2023-09-12 19:44:17 +04:00
}
2024-06-17 09:16:28 +03:00
Op::AddScaledOutput { id, scale } => {
let name = format!("output{id}");
if layout.outputs().any(|o| o.name() == name) {
return;
}
let output = Output::new(
name.clone(),
2024-06-17 09:16:28 +03:00
PhysicalProperties {
size: Size::from((1280, 720)),
subpixel: Subpixel::Unknown,
make: String::new(),
model: String::new(),
},
);
output.change_current_state(
Some(Mode {
size: Size::from((1280, 720)),
refresh: 60000,
}),
None,
Some(smithay::output::Scale::Fractional(scale)),
None,
);
output.user_data().insert_if_missing(|| OutputName {
connector: name,
make: None,
model: None,
serial: None,
});
2024-06-17 09:16:28 +03:00
layout.add_output(output.clone());
}
2023-09-12 19:44:17 +04:00
Op::RemoveOutput(id) => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
2023-09-12 19:44:17 +04:00
return;
};
layout.remove_output(&output);
2023-09-12 19:44:17 +04:00
}
Op::FocusOutput(id) => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
2023-09-12 19:44:17 +04:00
return;
};
layout.focus_output(&output);
2023-09-12 19:44:17 +04:00
}
2024-05-11 22:40:30 +02:00
Op::AddNamedWorkspace {
ws_name,
output_name,
} => {
layout.ensure_named_workspace(&WorkspaceConfig {
name: WorkspaceName(format!("ws{ws_name}")),
open_on_output: output_name.map(|name| format!("output{name}")),
});
}
Op::UnnameWorkspace { ws_name } => {
layout.unname_workspace(&format!("ws{ws_name}"));
}
2023-12-29 11:42:54 +04:00
Op::AddWindow {
id,
bbox,
min_max_size,
} => {
2024-10-14 17:36:56 +03:00
if layout.has_window(&id) {
return;
2023-10-14 20:42:10 +04:00
}
2023-12-29 11:42:54 +04:00
let win = TestWindow::new(id, bbox, min_max_size.0, min_max_size.1);
2024-11-29 21:11:02 +03:00
let is_floating = min_max_size.0.h > 0 && min_max_size.0.h == min_max_size.1.h;
layout.add_window(win, None, false, is_floating, ActivateWindow::default());
2023-09-12 19:44:17 +04:00
}
Op::AddWindowRightOf {
id,
right_of_id,
bbox,
min_max_size,
} => {
let mut found_right_of = false;
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &layout.interactive_move {
let win_id = move_.tile.window().0.id;
if win_id == id {
2024-07-15 15:51:48 +02:00
return;
}
if win_id == right_of_id {
found_right_of = true;
}
2024-07-15 15:51:48 +02:00
}
match &mut layout.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
for win in ws.windows() {
if win.0.id == id {
return;
}
if win.0.id == right_of_id {
found_right_of = true;
}
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
for win in ws.windows() {
if win.0.id == id {
return;
}
if win.0.id == right_of_id {
found_right_of = true;
}
}
}
}
}
if !found_right_of {
return;
}
let win = TestWindow::new(id, bbox, min_max_size.0, min_max_size.1);
2024-11-29 21:11:02 +03:00
let is_floating = min_max_size.0.h > 0 && min_max_size.0.h == min_max_size.1.h;
layout.add_window_right_of(&right_of_id, win, None, false, is_floating);
}
2024-05-11 22:40:30 +02:00
Op::AddWindowToNamedWorkspace {
id,
ws_name,
bbox,
min_max_size,
} => {
let ws_name = format!("ws{ws_name}");
let mut found_workspace = false;
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &layout.interactive_move {
if move_.tile.window().0.id == id {
return;
}
}
2024-05-11 22:40:30 +02:00
match &mut layout.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
for win in ws.windows() {
if win.0.id == id {
return;
}
}
2024-05-16 10:54:24 +04:00
if ws
.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(&ws_name))
{
2024-05-11 22:40:30 +02:00
found_workspace = true;
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
for win in ws.windows() {
if win.0.id == id {
return;
}
}
2024-05-16 10:54:24 +04:00
if ws
.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(&ws_name))
{
2024-05-11 22:40:30 +02:00
found_workspace = true;
}
}
}
}
if !found_workspace {
return;
}
let win = TestWindow::new(id, bbox, min_max_size.0, min_max_size.1);
2024-11-29 21:11:02 +03:00
let is_floating = min_max_size.0.h > 0 && min_max_size.0.h == min_max_size.1.h;
layout.add_window_to_named_workspace(
&ws_name,
win,
None,
false,
2024-11-29 21:11:02 +03:00
is_floating,
ActivateWindow::default(),
);
2024-05-11 22:40:30 +02:00
}
2023-09-12 19:44:17 +04:00
Op::CloseWindow(id) => {
2024-08-23 15:41:06 +03:00
layout.remove_window(&id, Transaction::new());
2023-09-12 19:44:17 +04:00
}
Op::FullscreenWindow(id) => {
layout.toggle_fullscreen(&id);
}
2024-06-04 19:44:33 +03:00
Op::SetFullscreenWindow {
window,
is_fullscreen,
} => {
layout.set_fullscreen(&window, is_fullscreen);
}
Op::FocusColumnLeft => layout.focus_left(),
Op::FocusColumnRight => layout.focus_right(),
2023-12-29 07:51:14 +04:00
Op::FocusColumnFirst => layout.focus_column_first(),
Op::FocusColumnLast => layout.focus_column_last(),
Op::FocusColumnRightOrFirst => layout.focus_column_right_or_first(),
Op::FocusColumnLeftOrLast => layout.focus_column_left_or_last(),
Op::FocusWindowOrMonitorUp(id) => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.focus_window_up_or_output(&output);
}
Op::FocusWindowOrMonitorDown(id) => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.focus_window_down_or_output(&output);
}
Op::FocusColumnOrMonitorLeft(id) => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.focus_column_left_or_output(&output);
}
Op::FocusColumnOrMonitorRight(id) => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.focus_column_right_or_output(&output);
}
Op::FocusWindowDown => layout.focus_down(),
Op::FocusWindowUp => layout.focus_up(),
Op::FocusWindowDownOrColumnLeft => layout.focus_down_or_left(),
Op::FocusWindowDownOrColumnRight => layout.focus_down_or_right(),
Op::FocusWindowUpOrColumnLeft => layout.focus_up_or_left(),
Op::FocusWindowUpOrColumnRight => layout.focus_up_or_right(),
Op::FocusWindowOrWorkspaceDown => layout.focus_window_or_workspace_down(),
Op::FocusWindowOrWorkspaceUp => layout.focus_window_or_workspace_up(),
Op::MoveColumnLeft => layout.move_left(),
Op::MoveColumnRight => layout.move_right(),
2023-12-29 08:01:02 +04:00
Op::MoveColumnToFirst => layout.move_column_to_first(),
Op::MoveColumnToLast => layout.move_column_to_last(),
Op::MoveColumnLeftOrToMonitorLeft(id) => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.move_column_left_or_to_output(&output);
}
Op::MoveColumnRightOrToMonitorRight(id) => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.move_column_right_or_to_output(&output);
}
Op::MoveWindowDown => layout.move_down(),
Op::MoveWindowUp => layout.move_up(),
Op::MoveWindowDownOrToWorkspaceDown => layout.move_down_or_to_workspace_down(),
Op::MoveWindowUpOrToWorkspaceUp => layout.move_up_or_to_workspace_up(),
Op::ConsumeOrExpelWindowLeft { id } => {
let id = id.filter(|id| layout.has_window(id));
layout.consume_or_expel_window_left(id.as_ref());
}
Op::ConsumeOrExpelWindowRight { id } => {
let id = id.filter(|id| layout.has_window(id));
layout.consume_or_expel_window_right(id.as_ref());
}
Op::ConsumeWindowIntoColumn => layout.consume_into_column(),
Op::ExpelWindowFromColumn => layout.expel_from_column(),
Op::CenterColumn => layout.center_column(),
Op::FocusWorkspaceDown => layout.switch_workspace_down(),
Op::FocusWorkspaceUp => layout.switch_workspace_up(),
Op::FocusWorkspace(idx) => layout.switch_workspace(idx),
2024-03-19 14:27:52 +00:00
Op::FocusWorkspaceAutoBackAndForth(idx) => {
layout.switch_workspace_auto_back_and_forth(idx)
}
Op::FocusWorkspacePrevious => layout.switch_workspace_previous(),
Op::MoveWindowToWorkspaceDown => layout.move_to_workspace_down(),
Op::MoveWindowToWorkspaceUp => layout.move_to_workspace_up(),
Op::MoveWindowToWorkspace {
window_id,
workspace_idx,
} => {
let window_id = window_id.filter(|id| {
layout
.active_monitor()
.map_or(false, |mon| mon.has_window(id))
});
layout.move_to_workspace(window_id.as_ref(), workspace_idx);
}
2024-01-15 10:31:44 +04:00
Op::MoveColumnToWorkspaceDown => layout.move_column_to_workspace_down(),
Op::MoveColumnToWorkspaceUp => layout.move_column_to_workspace_up(),
Op::MoveColumnToWorkspace(idx) => layout.move_column_to_workspace(idx),
Op::MoveWindowToOutput {
window_id,
output_id: id,
target_ws_idx,
} => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
let mon = layout.monitor_for_output(&output).unwrap();
let window_id = window_id.filter(|id| layout.has_window(id));
let target_ws_idx = target_ws_idx.filter(|idx| mon.workspaces.len() > *idx);
layout.move_to_output(window_id.as_ref(), &output, target_ws_idx);
}
2024-01-15 10:36:59 +04:00
Op::MoveColumnToOutput(id) => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.move_column_to_output(&output);
}
2023-10-14 20:42:10 +04:00
Op::MoveWorkspaceDown => layout.move_workspace_down(),
Op::MoveWorkspaceUp => layout.move_workspace_up(),
Op::SwitchPresetColumnWidth => layout.toggle_width(),
2024-09-12 11:53:10 +03:00
Op::SwitchPresetWindowHeight { id } => {
let id = id.filter(|id| layout.has_window(id));
layout.toggle_window_height(id.as_ref());
}
Op::MaximizeColumn => layout.toggle_full_width(),
Op::SetColumnWidth(change) => layout.set_column_width(change),
Op::SetWindowHeight { id, change } => {
let id = id.filter(|id| layout.has_window(id));
layout.set_window_height(id.as_ref(), change);
}
Op::ResetWindowHeight { id } => {
let id = id.filter(|id| layout.has_window(id));
layout.reset_window_height(id.as_ref());
}
2024-11-29 21:11:02 +03:00
Op::ToggleWindowFloating { id } => {
let id = id.filter(|id| layout.has_window(id));
layout.toggle_window_floating(id.as_ref());
}
Op::SwitchFocusFloatingTiling => {
layout.switch_focus_floating_tiling();
}
2023-09-28 08:59:45 +04:00
Op::Communicate(id) => {
let mut update = false;
2024-07-15 15:51:48 +02:00
if let Some(InteractiveMoveState::Moving(move_)) = &layout.interactive_move {
if move_.tile.window().0.id == id {
if move_.tile.window().communicate() {
update = true;
}
if update {
// FIXME: serial.
layout.update_window(&id, None);
}
return;
}
}
match &mut layout.monitor_set {
2023-09-28 08:59:45 +04:00
MonitorSet::Normal { monitors, .. } => {
'outer: for mon in monitors {
for ws in &mut mon.workspaces {
for win in ws.windows() {
if win.0.id == id {
if win.communicate() {
update = true;
2023-09-28 08:59:45 +04:00
}
break 'outer;
}
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
2023-09-28 08:59:45 +04:00
'outer: for ws in workspaces {
for win in ws.windows() {
if win.0.id == id {
if win.communicate() {
update = true;
2023-09-28 08:59:45 +04:00
}
break 'outer;
}
}
}
}
}
if update {
2024-05-10 16:58:53 +04:00
// FIXME: serial.
layout.update_window(&id, None);
2023-09-28 08:59:45 +04:00
}
}
Op::Refresh { is_active } => {
layout.refresh(is_active);
}
Op::AdvanceAnimations { msec_delta } => {
let mut now = layout.clock.now_unadjusted();
if msec_delta >= 0 {
now = now.saturating_add(Duration::from_millis(msec_delta as u64));
} else {
now = now.saturating_sub(Duration::from_millis(-msec_delta as u64));
}
layout.clock.set_unadjusted(now);
layout.advance_animations();
}
Op::MoveWorkspaceToOutput(id) => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.move_workspace_to_output(&output);
}
2024-05-11 14:01:48 +04:00
Op::ViewOffsetGestureBegin {
output_idx: id,
is_touchpad: normalize,
} => {
2024-02-29 08:56:20 +04:00
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
2024-05-11 14:01:48 +04:00
layout.view_offset_gesture_begin(&output, normalize);
2024-02-29 08:56:20 +04:00
}
2024-05-11 14:01:48 +04:00
Op::ViewOffsetGestureUpdate {
delta,
timestamp,
is_touchpad,
} => {
layout.view_offset_gesture_update(delta, timestamp, is_touchpad);
2024-02-29 08:56:20 +04:00
}
2024-05-11 14:01:48 +04:00
Op::ViewOffsetGestureEnd { is_touchpad } => {
2024-02-29 08:56:20 +04:00
// We don't handle cancels in this gesture.
2024-05-11 14:01:48 +04:00
layout.view_offset_gesture_end(false, is_touchpad);
2024-02-29 08:56:20 +04:00
}
2024-06-19 21:54:46 +03:00
Op::WorkspaceSwitchGestureBegin {
output_idx: id,
is_touchpad,
} => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
2024-06-19 21:54:46 +03:00
layout.workspace_switch_gesture_begin(&output, is_touchpad);
}
2024-06-19 21:54:46 +03:00
Op::WorkspaceSwitchGestureUpdate {
delta,
timestamp,
is_touchpad,
} => {
layout.workspace_switch_gesture_update(delta, timestamp, is_touchpad);
}
2024-06-19 21:54:46 +03:00
Op::WorkspaceSwitchGestureEnd {
cancelled,
is_touchpad,
} => {
layout.workspace_switch_gesture_end(cancelled, is_touchpad);
}
2024-07-15 15:51:48 +02:00
Op::InteractiveMoveBegin {
window,
output_idx,
px,
py,
} => {
let name = format!("output{output_idx}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.interactive_move_begin(window, &output, Point::from((px, py)));
}
Op::InteractiveMoveUpdate {
window,
dx,
dy,
output_idx,
px,
py,
} => {
let name = format!("output{output_idx}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.interactive_move_update(
&window,
Point::from((dx, dy)),
output,
Point::from((px, py)),
);
}
Op::InteractiveMoveEnd { window } => {
layout.interactive_move_end(&window);
}
2024-05-10 16:58:53 +04:00
Op::InteractiveResizeBegin { window, edges } => {
layout.interactive_resize_begin(window, edges);
}
Op::InteractiveResizeUpdate { window, dx, dy } => {
layout.interactive_resize_update(&window, Point::from((dx, dy)));
}
Op::InteractiveResizeEnd { window } => {
layout.interactive_resize_end(&window);
}
2023-09-12 19:44:17 +04:00
}
}
}
2023-09-26 20:41:55 +04:00
#[track_caller]
fn check_ops(ops: &[Op]) -> Layout<TestWindow> {
let mut layout = Layout::default();
2023-09-26 20:41:55 +04:00
for op in ops {
op.apply(&mut layout);
layout.verify_invariants();
2023-09-26 20:41:55 +04:00
}
layout
2023-09-26 20:41:55 +04:00
}
#[track_caller]
fn check_ops_with_options(options: Options, ops: &[Op]) -> Layout<TestWindow> {
let mut layout = Layout::with_options(Clock::with_time(Duration::ZERO), options);
for op in ops {
op.apply(&mut layout);
layout.verify_invariants();
}
layout
}
2023-09-12 19:44:17 +04:00
#[test]
fn operations_dont_panic() {
2023-09-12 19:44:17 +04:00
let every_op = [
Op::AddOutput(0),
Op::AddOutput(1),
Op::AddOutput(2),
Op::RemoveOutput(0),
Op::RemoveOutput(1),
Op::RemoveOutput(2),
Op::FocusOutput(0),
Op::FocusOutput(1),
Op::FocusOutput(2),
2024-05-11 22:40:30 +02:00
Op::AddNamedWorkspace {
ws_name: 1,
output_name: Some(1),
},
Op::UnnameWorkspace { ws_name: 1 },
2023-09-12 19:44:17 +04:00
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
2023-09-12 19:44:17 +04:00
},
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
2023-09-12 19:44:17 +04:00
},
2024-05-11 22:40:30 +02:00
Op::AddWindowRightOf {
2023-09-12 19:44:17 +04:00
id: 2,
2024-05-11 22:40:30 +02:00
right_of_id: 1,
2023-09-12 19:44:17 +04:00
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
2023-09-12 19:44:17 +04:00
},
2024-05-11 22:40:30 +02:00
Op::AddWindowToNamedWorkspace {
id: 3,
2024-05-11 22:40:30 +02:00
ws_name: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
2023-09-12 19:44:17 +04:00
Op::CloseWindow(0),
Op::CloseWindow(1),
Op::CloseWindow(2),
Op::FullscreenWindow(1),
Op::FullscreenWindow(2),
Op::FullscreenWindow(3),
2023-09-12 19:44:17 +04:00
Op::FocusColumnLeft,
Op::FocusColumnRight,
Op::FocusColumnRightOrFirst,
Op::FocusColumnLeftOrLast,
Op::FocusWindowOrMonitorUp(0),
Op::FocusWindowOrMonitorDown(1),
Op::FocusColumnOrMonitorLeft(0),
Op::FocusColumnOrMonitorRight(1),
Op::FocusWindowUp,
Op::FocusWindowUpOrColumnLeft,
Op::FocusWindowUpOrColumnRight,
Op::FocusWindowOrWorkspaceUp,
Op::FocusWindowDown,
Op::FocusWindowDownOrColumnLeft,
Op::FocusWindowDownOrColumnRight,
Op::FocusWindowOrWorkspaceDown,
2023-09-12 19:44:17 +04:00
Op::MoveColumnLeft,
Op::MoveColumnRight,
Op::MoveColumnLeftOrToMonitorLeft(0),
Op::MoveColumnRightOrToMonitorRight(1),
2023-09-12 19:44:17 +04:00
Op::ConsumeWindowIntoColumn,
Op::ExpelWindowFromColumn,
Op::CenterColumn,
2023-09-12 19:44:17 +04:00
Op::FocusWorkspaceDown,
Op::FocusWorkspaceUp,
2023-09-16 12:14:02 +04:00
Op::FocusWorkspace(1),
Op::FocusWorkspace(2),
2023-09-12 19:44:17 +04:00
Op::MoveWindowToWorkspaceDown,
Op::MoveWindowToWorkspaceUp,
Op::MoveWindowToWorkspace {
window_id: None,
workspace_idx: 1,
},
Op::MoveWindowToWorkspace {
window_id: None,
workspace_idx: 2,
},
2024-01-15 10:31:44 +04:00
Op::MoveColumnToWorkspaceDown,
Op::MoveColumnToWorkspaceUp,
Op::MoveColumnToWorkspace(1),
Op::MoveColumnToWorkspace(2),
2023-10-14 20:42:10 +04:00
Op::MoveWindowDown,
Op::MoveWindowDownOrToWorkspaceDown,
2023-10-14 20:42:10 +04:00
Op::MoveWindowUp,
Op::MoveWindowUpOrToWorkspaceUp,
Op::ConsumeOrExpelWindowLeft { id: None },
Op::ConsumeOrExpelWindowRight { id: None },
Op::MoveWorkspaceToOutput(1),
2023-09-12 19:44:17 +04:00
];
for third in every_op {
for second in every_op {
for first in every_op {
// eprintln!("{first:?}, {second:?}, {third:?}");
2023-09-12 19:44:17 +04:00
let mut layout = Layout::default();
first.apply(&mut layout);
layout.verify_invariants();
second.apply(&mut layout);
layout.verify_invariants();
third.apply(&mut layout);
layout.verify_invariants();
2023-09-12 19:44:17 +04:00
}
}
}
}
#[test]
fn operations_from_starting_state_dont_panic() {
if std::env::var_os("RUN_SLOW_TESTS").is_none() {
eprintln!("ignoring slow test");
return;
}
// Running every op from an empty state doesn't get us to all the interesting states. So,
// also run it from a manually-created starting state with more things going on to exercise
// more code paths.
let setup_ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::MoveWindowToWorkspaceDown,
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::AddWindow {
id: 3,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::FocusColumnLeft,
Op::ConsumeWindowIntoColumn,
Op::AddWindow {
id: 4,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::AddOutput(2),
Op::AddWindow {
id: 5,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::MoveWindowToOutput {
window_id: None,
output_id: 2,
target_ws_idx: None,
},
Op::FocusOutput(1),
Op::Communicate(1),
Op::Communicate(2),
Op::Communicate(3),
Op::Communicate(4),
Op::Communicate(5),
];
let every_op = [
Op::AddOutput(0),
Op::AddOutput(1),
Op::AddOutput(2),
Op::RemoveOutput(0),
Op::RemoveOutput(1),
Op::RemoveOutput(2),
Op::FocusOutput(0),
Op::FocusOutput(1),
Op::FocusOutput(2),
2024-05-11 22:40:30 +02:00
Op::AddNamedWorkspace {
ws_name: 1,
output_name: Some(1),
},
Op::UnnameWorkspace { ws_name: 1 },
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::AddWindowRightOf {
id: 6,
right_of_id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::AddWindowRightOf {
id: 7,
right_of_id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
2024-05-11 22:40:30 +02:00
Op::AddWindowToNamedWorkspace {
id: 5,
ws_name: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::CloseWindow(0),
Op::CloseWindow(1),
Op::CloseWindow(2),
Op::FullscreenWindow(1),
Op::FullscreenWindow(2),
Op::FullscreenWindow(3),
2024-06-04 19:44:33 +03:00
Op::SetFullscreenWindow {
window: 1,
is_fullscreen: false,
},
Op::SetFullscreenWindow {
window: 1,
is_fullscreen: true,
},
Op::SetFullscreenWindow {
window: 2,
is_fullscreen: false,
},
Op::SetFullscreenWindow {
window: 2,
is_fullscreen: true,
},
Op::FocusColumnLeft,
Op::FocusColumnRight,
Op::FocusColumnRightOrFirst,
Op::FocusColumnLeftOrLast,
Op::FocusWindowOrMonitorUp(0),
Op::FocusWindowOrMonitorDown(1),
Op::FocusColumnOrMonitorLeft(0),
Op::FocusColumnOrMonitorRight(1),
Op::FocusWindowUp,
Op::FocusWindowUpOrColumnLeft,
Op::FocusWindowUpOrColumnRight,
Op::FocusWindowOrWorkspaceUp,
Op::FocusWindowDown,
Op::FocusWindowDownOrColumnLeft,
Op::FocusWindowDownOrColumnRight,
Op::FocusWindowOrWorkspaceDown,
Op::MoveColumnLeft,
Op::MoveColumnRight,
Op::MoveColumnLeftOrToMonitorLeft(0),
Op::MoveColumnRightOrToMonitorRight(1),
Op::ConsumeWindowIntoColumn,
Op::ExpelWindowFromColumn,
Op::CenterColumn,
Op::FocusWorkspaceDown,
Op::FocusWorkspaceUp,
Op::FocusWorkspace(1),
Op::FocusWorkspace(2),
Op::FocusWorkspace(3),
Op::MoveWindowToWorkspaceDown,
Op::MoveWindowToWorkspaceUp,
Op::MoveWindowToWorkspace {
window_id: None,
workspace_idx: 1,
},
Op::MoveWindowToWorkspace {
window_id: None,
workspace_idx: 2,
},
Op::MoveWindowToWorkspace {
window_id: None,
workspace_idx: 3,
},
2024-01-15 10:31:44 +04:00
Op::MoveColumnToWorkspaceDown,
Op::MoveColumnToWorkspaceUp,
Op::MoveColumnToWorkspace(1),
Op::MoveColumnToWorkspace(2),
Op::MoveColumnToWorkspace(3),
2023-10-14 20:42:10 +04:00
Op::MoveWindowDown,
Op::MoveWindowDownOrToWorkspaceDown,
2023-10-14 20:42:10 +04:00
Op::MoveWindowUp,
Op::MoveWindowUpOrToWorkspaceUp,
Op::ConsumeOrExpelWindowLeft { id: None },
Op::ConsumeOrExpelWindowRight { id: None },
];
for third in every_op {
for second in every_op {
for first in every_op {
// eprintln!("{first:?}, {second:?}, {third:?}");
let mut layout = Layout::default();
for op in setup_ops {
op.apply(&mut layout);
}
first.apply(&mut layout);
layout.verify_invariants();
second.apply(&mut layout);
layout.verify_invariants();
third.apply(&mut layout);
layout.verify_invariants();
}
}
}
}
#[test]
fn primary_active_workspace_idx_not_updated_on_output_add() {
let ops = [
Op::AddOutput(1),
Op::AddOutput(2),
Op::FocusOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::FocusOutput(2),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::RemoveOutput(2),
Op::FocusWorkspace(3),
Op::AddOutput(2),
];
2023-09-26 20:41:55 +04:00
check_ops(&ops);
}
#[test]
2023-09-27 13:09:39 +04:00
fn window_closed_on_previous_workspace() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::FocusWorkspaceDown,
Op::CloseWindow(0),
];
check_ops(&ops);
}
2023-09-27 13:35:02 +04:00
#[test]
fn removing_output_must_keep_empty_focus_on_primary() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::AddOutput(2),
Op::RemoveOutput(1),
];
let layout = check_ops(&ops);
let MonitorSet::Normal { monitors, .. } = layout.monitor_set else {
unreachable!()
};
// The workspace from the removed output was inserted at position 0, so the active workspace
// must change to 1 to keep the focus on the empty workspace.
assert_eq!(monitors[0].active_workspace_idx, 1);
}
#[test]
fn move_to_workspace_by_idx_does_not_leave_empty_workspaces() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::AddOutput(2),
Op::FocusOutput(2),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::RemoveOutput(1),
Op::MoveWindowToWorkspace {
window_id: Some(0),
workspace_idx: 2,
},
];
let layout = check_ops(&ops);
let MonitorSet::Normal { monitors, .. } = layout.monitor_set else {
unreachable!()
};
assert!(monitors[0].workspaces[1].has_windows());
}
#[test]
fn empty_workspaces_dont_move_back_to_original_output() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::FocusWorkspaceDown,
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::AddOutput(2),
Op::RemoveOutput(1),
Op::FocusWorkspace(1),
Op::CloseWindow(1),
Op::AddOutput(1),
];
check_ops(&ops);
}
#[test]
fn large_negative_height_change() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
2023-12-29 11:42:54 +04:00
min_max_size: Default::default(),
},
Op::SetWindowHeight {
id: None,
change: SizeChange::AdjustProportion(-1e129),
},
];
let mut options = Options::default();
options.border.off = false;
2024-06-17 09:16:28 +03:00
options.border.width = FloatOrInt(1.);
check_ops_with_options(options, &ops);
}
2023-12-29 11:42:54 +04:00
#[test]
fn large_max_size() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
];
let mut options = Options::default();
options.border.off = false;
2024-06-17 09:16:28 +03:00
options.border.width = FloatOrInt(1.);
2023-12-29 11:42:54 +04:00
check_ops_with_options(options, &ops);
}
#[test]
fn workspace_cleanup_during_switch() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
Op::FocusWorkspaceDown,
Op::CloseWindow(1),
];
check_ops(&ops);
}
#[test]
fn workspace_transfer_during_switch() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
Op::AddOutput(2),
Op::FocusOutput(2),
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
Op::RemoveOutput(1),
Op::FocusWorkspaceDown,
Op::FocusWorkspaceDown,
Op::AddOutput(1),
];
check_ops(&ops);
}
#[test]
fn workspace_transfer_during_switch_from_last() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
Op::AddOutput(2),
Op::RemoveOutput(1),
Op::FocusWorkspaceUp,
Op::AddOutput(1),
];
check_ops(&ops);
}
#[test]
fn workspace_transfer_during_switch_gets_cleaned_up() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
Op::RemoveOutput(1),
Op::AddOutput(2),
Op::MoveColumnToWorkspaceDown,
Op::MoveColumnToWorkspaceDown,
Op::AddOutput(1),
];
check_ops(&ops);
}
#[test]
fn move_workspace_to_output() {
let ops = [
Op::AddOutput(1),
Op::AddOutput(2),
Op::FocusOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::MoveWorkspaceToOutput(2),
];
let layout = check_ops(&ops);
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = layout.monitor_set
else {
unreachable!()
};
assert_eq!(active_monitor_idx, 1);
assert_eq!(monitors[0].workspaces.len(), 1);
assert!(!monitors[0].workspaces[0].has_windows());
assert_eq!(monitors[1].active_workspace_idx, 0);
assert_eq!(monitors[1].workspaces.len(), 2);
assert!(monitors[1].workspaces[0].has_windows());
}
#[test]
fn fullscreen() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
Op::FullscreenWindow(1),
];
check_ops(&ops);
}
2024-06-04 19:44:33 +03:00
#[test]
fn unfullscreen_window_in_column() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
Op::ConsumeOrExpelWindowLeft { id: None },
2024-06-04 19:44:33 +03:00
Op::SetFullscreenWindow {
window: 2,
is_fullscreen: false,
},
];
check_ops(&ops);
}
#[test]
fn open_right_of_on_different_workspace() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
Op::FocusWorkspaceDown,
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
Op::AddWindowRightOf {
id: 3,
right_of_id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
];
let layout = check_ops(&ops);
let MonitorSet::Normal { monitors, .. } = layout.monitor_set else {
unreachable!()
};
let mon = monitors.into_iter().next().unwrap();
assert_eq!(
mon.active_workspace_idx, 1,
"the second workspace must remain active"
);
assert_eq!(
2024-11-30 09:18:33 +03:00
mon.workspaces[0].scrolling().active_column_idx(),
1,
"the new window must become active"
);
}
#[test]
// empty_workspace_above_first = true
fn open_right_of_on_different_workspace_ewaf() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
Op::FocusWorkspaceDown,
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
Op::AddWindowRightOf {
id: 3,
right_of_id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
},
];
let options = Options {
empty_workspace_above_first: true,
..Default::default()
};
let layout = check_ops_with_options(options, &ops);
let MonitorSet::Normal { monitors, .. } = layout.monitor_set else {
unreachable!()
};
let mon = monitors.into_iter().next().unwrap();
assert_eq!(
mon.active_workspace_idx, 2,
"the second workspace must remain active"
);
assert_eq!(
2024-11-30 09:18:33 +03:00
mon.workspaces[1].scrolling().active_column_idx(),
1,
"the new window must become active"
);
}
2024-04-13 18:17:49 +04:00
#[test]
fn unfullscreen_view_offset_not_reset_on_removal() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::FullscreenWindow(0),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::ConsumeOrExpelWindowRight { id: None },
2024-04-13 18:17:49 +04:00
];
check_ops(&ops);
}
#[test]
fn unfullscreen_view_offset_not_reset_on_consume() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::FullscreenWindow(0),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::ConsumeWindowIntoColumn,
];
check_ops(&ops);
}
#[test]
fn unfullscreen_view_offset_not_reset_on_quick_double_toggle() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::FullscreenWindow(0),
Op::FullscreenWindow(0),
];
check_ops(&ops);
}
#[test]
fn unfullscreen_view_offset_set_on_fullscreening_inactive_tile_in_column() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::ConsumeOrExpelWindowLeft { id: None },
2024-04-13 18:17:49 +04:00
Op::FullscreenWindow(0),
];
check_ops(&ops);
}
#[test]
fn unfullscreen_view_offset_not_reset_on_gesture() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (200, 200)),
min_max_size: Default::default(),
},
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (1280, 200)),
min_max_size: Default::default(),
},
Op::FullscreenWindow(1),
2024-05-11 14:01:48 +04:00
Op::ViewOffsetGestureBegin {
output_idx: 1,
is_touchpad: true,
},
Op::ViewOffsetGestureEnd {
is_touchpad: Some(true),
},
2024-04-13 18:17:49 +04:00
];
check_ops(&ops);
}
#[test]
fn removing_all_outputs_preserves_empty_named_workspaces() {
let ops = [
Op::AddOutput(1),
Op::AddNamedWorkspace {
ws_name: 1,
output_name: None,
},
Op::AddNamedWorkspace {
ws_name: 2,
output_name: None,
},
Op::RemoveOutput(1),
];
let layout = check_ops(&ops);
let MonitorSet::NoOutputs { workspaces } = layout.monitor_set else {
unreachable!()
};
assert_eq!(workspaces.len(), 2);
}
#[test]
fn config_change_updates_cached_sizes() {
let mut config = Config::default();
config.layout.border.off = false;
2024-06-17 09:16:28 +03:00
config.layout.border.width = FloatOrInt(2.);
let mut layout = Layout::new(Clock::default(), &config);
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (1280, 200)),
min_max_size: Default::default(),
}
.apply(&mut layout);
2024-06-17 09:16:28 +03:00
config.layout.border.width = FloatOrInt(4.);
layout.update_config(&config);
layout.verify_invariants();
}
2024-09-05 23:37:10 +02:00
#[test]
fn preset_height_change_removes_preset() {
let mut config = Config::default();
config.layout.preset_window_heights = vec![PresetSize::Fixed(1), PresetSize::Fixed(2)];
let mut layout = Layout::new(Clock::default(), &config);
2024-09-05 23:37:10 +02:00
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (1280, 200)),
min_max_size: Default::default(),
},
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (1280, 200)),
min_max_size: Default::default(),
},
Op::ConsumeOrExpelWindowLeft { id: None },
2024-09-12 11:53:10 +03:00
Op::SwitchPresetWindowHeight { id: None },
Op::SwitchPresetWindowHeight { id: None },
2024-09-05 23:37:10 +02:00
];
for op in ops {
op.apply(&mut layout);
}
// Leave only one.
config.layout.preset_window_heights = vec![PresetSize::Fixed(1)];
layout.update_config(&config);
layout.verify_invariants();
}
#[test]
fn set_window_height_recomputes_to_auto() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::ConsumeOrExpelWindowLeft { id: None },
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::ConsumeOrExpelWindowLeft { id: None },
Op::SetWindowHeight {
id: None,
change: SizeChange::SetFixed(100),
},
Op::FocusWindowUp,
Op::SetWindowHeight {
id: None,
change: SizeChange::SetFixed(200),
},
];
check_ops(&ops);
}
#[test]
fn one_window_in_column_becomes_weight_1() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::ConsumeOrExpelWindowLeft { id: None },
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::ConsumeOrExpelWindowLeft { id: None },
Op::SetWindowHeight {
id: None,
change: SizeChange::SetFixed(100),
},
Op::Communicate(2),
Op::FocusWindowUp,
Op::SetWindowHeight {
id: None,
change: SizeChange::SetFixed(200),
},
Op::Communicate(1),
Op::CloseWindow(0),
Op::CloseWindow(1),
];
check_ops(&ops);
}
#[test]
fn one_window_in_column_becomes_weight_1_after_fullscreen() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::ConsumeOrExpelWindowLeft { id: None },
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::ConsumeOrExpelWindowLeft { id: None },
Op::SetWindowHeight {
id: None,
change: SizeChange::SetFixed(100),
},
Op::Communicate(2),
Op::FocusWindowUp,
Op::SetWindowHeight {
id: None,
change: SizeChange::SetFixed(200),
},
Op::Communicate(1),
Op::CloseWindow(0),
Op::FullscreenWindow(1),
];
check_ops(&ops);
}
#[test]
fn fixed_height_takes_max_non_auto_into_account() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
2024-09-12 20:54:44 +03:00
Op::SetWindowHeight {
id: Some(0),
change: SizeChange::SetFixed(704),
},
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::ConsumeOrExpelWindowLeft { id: None },
];
let options = Options {
border: niri_config::Border {
off: false,
width: niri_config::FloatOrInt(4.),
..Default::default()
},
gaps: 0.,
..Default::default()
};
check_ops_with_options(options, &ops);
}
2024-07-15 15:51:48 +02:00
#[test]
fn start_interactive_move_then_remove_window() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::InteractiveMoveBegin {
window: 0,
output_idx: 1,
px: 0.,
py: 0.,
},
Op::CloseWindow(0),
];
check_ops(&ops);
}
2024-11-23 11:27:27 +03:00
#[test]
fn interactive_move_onto_empty_output() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::InteractiveMoveBegin {
window: 0,
output_idx: 1,
px: 0.,
py: 0.,
},
Op::AddOutput(2),
Op::InteractiveMoveUpdate {
window: 0,
dx: 1000.,
dy: 0.,
output_idx: 2,
px: 0.,
py: 0.,
},
Op::InteractiveMoveEnd { window: 0 },
];
check_ops(&ops);
}
#[test]
fn interactive_move_onto_empty_output_ewaf() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::InteractiveMoveBegin {
window: 0,
output_idx: 1,
px: 0.,
py: 0.,
},
Op::AddOutput(2),
Op::InteractiveMoveUpdate {
window: 0,
dx: 1000.,
dy: 0.,
output_idx: 2,
px: 0.,
py: 0.,
},
Op::InteractiveMoveEnd { window: 0 },
];
let options = Options {
empty_workspace_above_first: true,
..Default::default()
};
check_ops_with_options(options, &ops);
}
#[test]
fn interactive_move_onto_last_workspace() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::InteractiveMoveBegin {
window: 0,
output_idx: 1,
px: 0.,
py: 0.,
},
Op::InteractiveMoveUpdate {
window: 0,
dx: 1000.,
dy: 0.,
output_idx: 1,
px: 0.,
py: 0.,
},
Op::FocusWorkspaceDown,
Op::AdvanceAnimations { msec_delta: 1000 },
Op::InteractiveMoveEnd { window: 0 },
2024-11-23 11:27:27 +03:00
];
check_ops(&ops);
}
#[test]
fn interactive_move_onto_first_empty_workspace() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::InteractiveMoveBegin {
window: 1,
output_idx: 1,
px: 0.,
py: 0.,
},
Op::InteractiveMoveUpdate {
window: 1,
dx: 1000.,
dy: 0.,
output_idx: 1,
px: 0.,
py: 0.,
},
Op::FocusWorkspaceUp,
Op::AdvanceAnimations { msec_delta: 1000 },
Op::InteractiveMoveEnd { window: 1 },
];
let options = Options {
empty_workspace_above_first: true,
..Default::default()
};
check_ops_with_options(options, &ops);
}
#[test]
fn output_active_workspace_is_preserved() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::FocusWorkspaceDown,
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::RemoveOutput(1),
Op::AddOutput(1),
];
let layout = check_ops(&ops);
let MonitorSet::Normal { monitors, .. } = layout.monitor_set else {
unreachable!()
};
assert_eq!(monitors[0].active_workspace_idx, 1);
}
#[test]
fn output_active_workspace_is_preserved_with_other_outputs() {
let ops = [
Op::AddOutput(1),
Op::AddOutput(2),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::FocusWorkspaceDown,
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::RemoveOutput(1),
Op::AddOutput(1),
];
let layout = check_ops(&ops);
let MonitorSet::Normal { monitors, .. } = layout.monitor_set else {
unreachable!()
};
assert_eq!(monitors[1].active_workspace_idx, 1);
}
#[test]
fn named_workspace_to_output() {
let ops = [
Op::AddNamedWorkspace {
ws_name: 1,
output_name: None,
},
Op::AddOutput(1),
Op::MoveWorkspaceToOutput(1),
Op::FocusWorkspaceUp,
];
check_ops(&ops);
}
#[test]
// empty_workspace_above_first = true
fn named_workspace_to_output_ewaf() {
let ops = [
Op::AddNamedWorkspace {
ws_name: 1,
output_name: Some(2),
},
Op::AddOutput(1),
Op::AddOutput(2),
];
let options = Options {
empty_workspace_above_first: true,
..Default::default()
};
check_ops_with_options(options, &ops);
}
#[test]
fn move_window_to_empty_workspace_above_first() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::MoveWorkspaceUp,
Op::MoveWorkspaceDown,
Op::FocusWorkspaceUp,
Op::MoveWorkspaceDown,
];
let options = Options {
empty_workspace_above_first: true,
..Default::default()
};
check_ops_with_options(options, &ops);
}
#[test]
fn move_window_to_different_output() {
let ops = [
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::AddOutput(1),
Op::AddOutput(2),
Op::MoveWorkspaceToOutput(2),
];
let options = Options {
empty_workspace_above_first: true,
..Default::default()
};
check_ops_with_options(options, &ops);
}
#[test]
fn close_window_empty_ws_above_first() {
let ops = [
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::AddOutput(1),
Op::CloseWindow(1),
];
let options = Options {
empty_workspace_above_first: true,
..Default::default()
};
check_ops_with_options(options, &ops);
}
#[test]
fn add_and_remove_output() {
let ops = [
Op::AddOutput(2),
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::RemoveOutput(2),
];
let options = Options {
empty_workspace_above_first: true,
..Default::default()
};
check_ops_with_options(options, &ops);
}
#[test]
fn switch_ewaf_on() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
];
let mut layout = check_ops(&ops);
layout.update_options(Options {
empty_workspace_above_first: true,
..Default::default()
});
layout.verify_invariants();
}
#[test]
fn switch_ewaf_off() {
let ops = [
Op::AddOutput(1),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
];
let options = Options {
empty_workspace_above_first: true,
..Default::default()
};
let mut layout = check_ops_with_options(options, &ops);
layout.update_options(Options::default());
layout.verify_invariants();
}
#[test]
fn interactive_move_drop_on_other_output_during_animation() {
let ops = [
Op::AddOutput(3),
Op::AddWindow {
id: 3,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::InteractiveMoveBegin {
window: 3,
output_idx: 3,
px: 0.0,
py: 0.0,
},
Op::FocusWorkspaceDown,
Op::AddOutput(4),
Op::InteractiveMoveUpdate {
window: 3,
dx: 0.0,
dy: 8300.68619826683,
output_idx: 4,
px: 0.0,
py: 0.0,
},
Op::RemoveOutput(4),
Op::InteractiveMoveEnd { window: 3 },
];
check_ops(&ops);
}
2024-11-29 21:11:02 +03:00
#[test]
fn set_width_fixed_negative() {
let ops = [
Op::AddOutput(3),
Op::AddWindow {
id: 3,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::ToggleWindowFloating { id: Some(3) },
Op::SetColumnWidth(SizeChange::SetFixed(-100)),
];
check_ops(&ops);
}
#[test]
fn set_height_fixed_negative() {
let ops = [
Op::AddOutput(3),
Op::AddWindow {
id: 3,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::ToggleWindowFloating { id: Some(3) },
Op::SetWindowHeight {
id: None,
change: SizeChange::SetFixed(-100),
},
];
check_ops(&ops);
}
#[test]
fn interactive_resize_to_negative() {
let ops = [
Op::AddOutput(3),
Op::AddWindow {
id: 3,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::ToggleWindowFloating { id: Some(3) },
Op::InteractiveResizeBegin {
window: 3,
edges: ResizeEdge::BOTTOM_RIGHT,
},
Op::InteractiveResizeUpdate {
window: 3,
dx: -10000.,
dy: -10000.,
},
];
check_ops(&ops);
}
#[test]
fn windows_on_other_workspaces_remain_activated() {
let ops = [
Op::AddOutput(3),
Op::AddWindow {
id: 3,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::FocusWorkspaceDown,
Op::Refresh { is_active: true },
];
let layout = check_ops(&ops);
let (_, win) = layout.windows().next().unwrap();
assert!(win.0.pending_activated.get());
}
2024-06-17 09:16:28 +03:00
fn arbitrary_spacing() -> impl Strategy<Value = f64> {
// Give equal weight to:
// - 0: the element is disabled
// - 4: some reasonable value
// - random value, likely unreasonably big
2024-06-17 09:16:28 +03:00
prop_oneof![Just(0.), Just(4.), ((1.)..=65535.)]
}
2024-07-22 12:35:57 +03:00
fn arbitrary_spacing_neg() -> impl Strategy<Value = f64> {
// Give equal weight to:
// - 0: the element is disabled
// - 4: some reasonable value
// - -4: some reasonable negative value
// - random value, likely unreasonably big
prop_oneof![Just(0.), Just(4.), Just(-4.), ((1.)..=65535.)]
}
fn arbitrary_struts() -> impl Strategy<Value = Struts> {
(
2024-07-22 12:35:57 +03:00
arbitrary_spacing_neg(),
arbitrary_spacing_neg(),
arbitrary_spacing_neg(),
arbitrary_spacing_neg(),
)
.prop_map(|(left, right, top, bottom)| Struts {
2024-06-17 09:16:28 +03:00
left: FloatOrInt(left),
right: FloatOrInt(right),
top: FloatOrInt(top),
bottom: FloatOrInt(bottom),
})
}
fn arbitrary_center_focused_column() -> impl Strategy<Value = CenterFocusedColumn> {
prop_oneof![
Just(CenterFocusedColumn::Never),
Just(CenterFocusedColumn::OnOverflow),
Just(CenterFocusedColumn::Always),
]
}
prop_compose! {
fn arbitrary_focus_ring()(
off in any::<bool>(),
width in arbitrary_spacing(),
) -> niri_config::FocusRing {
niri_config::FocusRing {
off,
2024-06-17 09:16:28 +03:00
width: FloatOrInt(width),
..Default::default()
}
}
}
prop_compose! {
fn arbitrary_border()(
off in any::<bool>(),
width in arbitrary_spacing(),
) -> niri_config::Border {
niri_config::Border {
off,
2024-06-17 09:16:28 +03:00
width: FloatOrInt(width),
..Default::default()
}
}
}
prop_compose! {
fn arbitrary_options()(
gaps in arbitrary_spacing(),
struts in arbitrary_struts(),
focus_ring in arbitrary_focus_ring(),
border in arbitrary_border(),
center_focused_column in arbitrary_center_focused_column(),
always_center_single_column in any::<bool>(),
empty_workspace_above_first in any::<bool>(),
) -> Options {
Options {
2024-06-17 09:16:28 +03:00
gaps,
struts,
center_focused_column,
always_center_single_column,
empty_workspace_above_first,
focus_ring,
border,
..Default::default()
}
}
2023-12-29 11:43:16 +04:00
}
2023-09-27 13:35:02 +04:00
proptest! {
#![proptest_config(ProptestConfig {
cases: if std::env::var_os("RUN_SLOW_TESTS").is_none() {
eprintln!("ignoring slow test");
0
} else {
ProptestConfig::default().cases
},
..ProptestConfig::default()
})]
#[test]
fn random_operations_dont_panic(
ops: Vec<Op>,
options in arbitrary_options(),
post_options in prop::option::of(arbitrary_options()),
) {
2023-09-27 13:35:02 +04:00
// eprintln!("{ops:?}");
let mut layout = check_ops_with_options(options, &ops);
if let Some(post_options) = post_options {
layout.update_options(post_options);
layout.verify_invariants();
}
2023-09-27 13:35:02 +04:00
}
}
2023-09-12 19:44:17 +04:00
}