aboutsummaryrefslogtreecommitdiff
path: root/src/layout/tests.rs
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-01-31 17:55:15 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-01-31 17:56:43 +0300
commit49ddf66c2f77d6dab8bdb84de7345b9c3a28f9df (patch)
treef4eb339823bf84b1b70da4c7b51910045dbe6dba /src/layout/tests.rs
parenta169e0335d2773edd31a1959a3ff4fca994a422a (diff)
downloadniri-49ddf66c2f77d6dab8bdb84de7345b9c3a28f9df.tar.gz
niri-49ddf66c2f77d6dab8bdb84de7345b9c3a28f9df.tar.bz2
niri-49ddf66c2f77d6dab8bdb84de7345b9c3a28f9df.zip
layout: Move tests to separate file
This way changing just the tests won't rebuild the main library.
Diffstat (limited to 'src/layout/tests.rs')
-rw-r--r--src/layout/tests.rs3163
1 files changed, 3163 insertions, 0 deletions
diff --git a/src/layout/tests.rs b/src/layout/tests.rs
new file mode 100644
index 00000000..47484817
--- /dev/null
+++ b/src/layout/tests.rs
@@ -0,0 +1,3163 @@
+use std::cell::Cell;
+
+use niri_config::{FloatOrInt, OutputName, WorkspaceName, WorkspaceReference};
+use proptest::prelude::*;
+use proptest_derive::Arbitrary;
+use smithay::output::{Mode, PhysicalProperties, Subpixel};
+use smithay::utils::Rectangle;
+
+use super::*;
+
+impl<W: LayoutElement> Default for Layout<W> {
+ fn default() -> Self {
+ Self::with_options(Clock::with_time(Duration::ZERO), Default::default())
+ }
+}
+
+#[derive(Debug)]
+struct TestWindowInner {
+ id: usize,
+ parent_id: Cell<Option<usize>>,
+ bbox: Cell<Rectangle<i32, Logical>>,
+ initial_bbox: Rectangle<i32, Logical>,
+ requested_size: Cell<Option<Size<i32, Logical>>>,
+ min_size: Size<i32, Logical>,
+ max_size: Size<i32, Logical>,
+ pending_fullscreen: Cell<bool>,
+ pending_activated: Cell<bool>,
+}
+
+#[derive(Debug, Clone)]
+struct TestWindow(Rc<TestWindowInner>);
+
+#[derive(Debug, Clone, Copy, Arbitrary)]
+struct TestWindowParams {
+ #[proptest(strategy = "1..=5usize")]
+ id: usize,
+ #[proptest(strategy = "arbitrary_parent_id()")]
+ parent_id: Option<usize>,
+ is_floating: bool,
+ #[proptest(strategy = "arbitrary_bbox()")]
+ bbox: Rectangle<i32, Logical>,
+ #[proptest(strategy = "arbitrary_min_max_size()")]
+ min_max_size: (Size<i32, Logical>, Size<i32, Logical>),
+}
+
+impl TestWindowParams {
+ pub fn new(id: usize) -> Self {
+ Self {
+ id,
+ parent_id: None,
+ is_floating: false,
+ bbox: Rectangle::from_size(Size::from((100, 200))),
+ min_max_size: Default::default(),
+ }
+ }
+}
+
+impl TestWindow {
+ fn new(params: TestWindowParams) -> Self {
+ Self(Rc::new(TestWindowInner {
+ id: params.id,
+ parent_id: Cell::new(params.parent_id),
+ bbox: Cell::new(params.bbox),
+ initial_bbox: params.bbox,
+ requested_size: Cell::new(None),
+ min_size: params.min_max_size.0,
+ max_size: params.min_max_size.1,
+ pending_fullscreen: Cell::new(false),
+ pending_activated: Cell::new(false),
+ }))
+ }
+
+ fn communicate(&self) -> bool {
+ if let Some(size) = self.0.requested_size.get() {
+ 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
+ }
+}
+
+impl LayoutElement for TestWindow {
+ type Id = usize;
+
+ fn id(&self) -> &Self::Id {
+ &self.0.id
+ }
+
+ fn size(&self) -> Size<i32, Logical> {
+ self.0.bbox.get().size
+ }
+
+ fn buf_loc(&self) -> Point<i32, Logical> {
+ (0, 0).into()
+ }
+
+ fn is_in_input_region(&self, _point: Point<f64, Logical>) -> bool {
+ false
+ }
+
+ fn render<R: NiriRenderer>(
+ &self,
+ _renderer: &mut R,
+ _location: Point<f64, Logical>,
+ _scale: Scale<f64>,
+ _alpha: f32,
+ _target: RenderTarget,
+ ) -> SplitElements<LayoutElementRenderElement<R>> {
+ SplitElements::default()
+ }
+
+ fn request_size(
+ &mut self,
+ size: Size<i32, Logical>,
+ _animate: bool,
+ _transaction: Option<Transaction>,
+ ) {
+ self.0.requested_size.set(Some(size));
+ self.0.pending_fullscreen.set(false);
+ }
+
+ fn request_fullscreen(&mut self, _size: Size<i32, Logical>) {
+ self.0.pending_fullscreen.set(true);
+ }
+
+ fn min_size(&self) -> Size<i32, Logical> {
+ self.0.min_size
+ }
+
+ fn max_size(&self) -> Size<i32, Logical> {
+ self.0.max_size
+ }
+
+ fn is_wl_surface(&self, _wl_surface: &WlSurface) -> bool {
+ false
+ }
+
+ fn set_preferred_scale_transform(&self, _scale: output::Scale, _transform: Transform) {}
+
+ fn has_ssd(&self) -> bool {
+ false
+ }
+
+ fn output_enter(&self, _output: &Output) {}
+
+ fn output_leave(&self, _output: &Output) {}
+
+ fn set_offscreen_element_id(&self, _id: Option<Id>) {}
+
+ fn set_activated(&mut self, active: bool) {
+ self.0.pending_activated.set(active);
+ }
+
+ fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
+
+ fn is_ignoring_opacity_window_rule(&self) -> bool {
+ false
+ }
+
+ fn configure_intent(&self) -> ConfigureIntent {
+ ConfigureIntent::CanSend
+ }
+
+ fn send_pending_configure(&mut self) {}
+
+ fn set_active_in_column(&mut self, _active: bool) {}
+
+ fn set_floating(&mut self, _floating: bool) {}
+
+ fn is_fullscreen(&self) -> bool {
+ false
+ }
+
+ fn is_pending_fullscreen(&self) -> bool {
+ self.0.pending_fullscreen.get()
+ }
+
+ fn requested_size(&self) -> Option<Size<i32, Logical>> {
+ self.0.requested_size.get()
+ }
+
+ fn is_child_of(&self, parent: &Self) -> bool {
+ self.0.parent_id.get() == Some(parent.0.id)
+ }
+
+ fn refresh(&self) {}
+
+ fn rules(&self) -> &ResolvedWindowRules {
+ static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
+ &EMPTY
+ }
+
+ fn animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot> {
+ None
+ }
+
+ fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> {
+ None
+ }
+
+ fn set_interactive_resize(&mut self, _data: Option<InteractiveResizeData>) {}
+
+ fn cancel_interactive_resize(&mut self) {}
+
+ fn on_commit(&mut self, _serial: Serial) {}
+
+ fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
+ None
+ }
+}
+
+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()));
+ let size: Size<i32, _> = Size::from((w.max(1).into(), h.max(1).into()));
+ Rectangle::new(loc, size)
+ })
+}
+
+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),
+ // Interactive resize can have negative values here.
+ Just(SizeChange::SetFixed(-100)),
+ ]
+}
+
+fn arbitrary_position_change() -> impl Strategy<Value = PositionChange> {
+ prop_oneof![
+ (-1000f64..1000f64).prop_map(PositionChange::SetFixed),
+ (-1000f64..1000f64).prop_map(PositionChange::AdjustFixed),
+ any::<f64>().prop_map(PositionChange::SetFixed),
+ any::<f64>().prop_map(PositionChange::AdjustFixed),
+ ]
+}
+
+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>)> {
+ 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)
+ }),
+ ]
+}
+
+fn arbitrary_view_offset_gesture_delta() -> impl Strategy<Value = f64> {
+ prop_oneof![(-10f64..10f64), (-50000f64..50000f64),]
+}
+
+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()),
+ ]
+}
+
+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),
+ ]
+}
+
+fn arbitrary_parent_id() -> impl Strategy<Value = Option<usize>> {
+ prop_oneof![
+ 5 => Just(None),
+ 1 => prop::option::of(1..=5usize),
+ ]
+}
+
+fn arbitrary_scroll_direction() -> impl Strategy<Value = ScrollDirection> {
+ prop_oneof![Just(ScrollDirection::Left), Just(ScrollDirection::Right)]
+}
+
+#[derive(Debug, Clone, Copy, Arbitrary)]
+enum Op {
+ AddOutput(#[proptest(strategy = "1..=5usize")] usize),
+ AddScaledOutput {
+ #[proptest(strategy = "1..=5usize")]
+ id: usize,
+ #[proptest(strategy = "arbitrary_scale()")]
+ scale: f64,
+ },
+ RemoveOutput(#[proptest(strategy = "1..=5usize")] usize),
+ FocusOutput(#[proptest(strategy = "1..=5usize")] usize),
+ 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,
+ },
+ AddWindow {
+ params: TestWindowParams,
+ },
+ AddWindowNextTo {
+ params: TestWindowParams,
+ #[proptest(strategy = "1..=5usize")]
+ next_to_id: usize,
+ },
+ AddWindowToNamedWorkspace {
+ params: TestWindowParams,
+ #[proptest(strategy = "1..=5usize")]
+ ws_name: usize,
+ },
+ CloseWindow(#[proptest(strategy = "1..=5usize")] usize),
+ FullscreenWindow(#[proptest(strategy = "1..=5usize")] usize),
+ SetFullscreenWindow {
+ #[proptest(strategy = "1..=5usize")]
+ window: usize,
+ is_fullscreen: bool,
+ },
+ FocusColumnLeft,
+ FocusColumnRight,
+ 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,
+ FocusWindow(#[proptest(strategy = "1..=5usize")] usize),
+ MoveColumnLeft,
+ MoveColumnRight,
+ 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>,
+ },
+ ConsumeWindowIntoColumn,
+ ExpelWindowFromColumn,
+ SwapWindowInDirection(#[proptest(strategy = "arbitrary_scroll_direction()")] ScrollDirection),
+ CenterColumn,
+ CenterWindow {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ id: Option<usize>,
+ },
+ FocusWorkspaceDown,
+ FocusWorkspaceUp,
+ FocusWorkspace(#[proptest(strategy = "0..=4usize")] usize),
+ FocusWorkspaceAutoBackAndForth(#[proptest(strategy = "0..=4usize")] usize),
+ FocusWorkspacePrevious,
+ MoveWindowToWorkspaceDown,
+ MoveWindowToWorkspaceUp,
+ MoveWindowToWorkspace {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ window_id: Option<usize>,
+ #[proptest(strategy = "0..=4usize")]
+ workspace_idx: usize,
+ },
+ MoveColumnToWorkspaceDown,
+ MoveColumnToWorkspaceUp,
+ MoveColumnToWorkspace(#[proptest(strategy = "0..=4usize")] usize),
+ MoveWorkspaceDown,
+ MoveWorkspaceUp,
+ MoveWorkspaceToIndex {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ ws_name: Option<usize>,
+ #[proptest(strategy = "0..=4usize")]
+ target_idx: usize,
+ },
+ MoveWorkspaceToMonitor {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ ws_name: Option<usize>,
+ #[proptest(strategy = "0..=5usize")]
+ output_id: usize,
+ },
+ SetWorkspaceName {
+ #[proptest(strategy = "1..=5usize")]
+ new_ws_name: usize,
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ ws_name: Option<usize>,
+ },
+ UnsetWorkspaceName {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ ws_name: Option<usize>,
+ },
+ 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,
+ SwitchPresetWindowWidth {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ id: Option<usize>,
+ },
+ SwitchPresetWindowHeight {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ id: Option<usize>,
+ },
+ MaximizeColumn,
+ SetColumnWidth(#[proptest(strategy = "arbitrary_size_change()")] SizeChange),
+ SetWindowWidth {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ id: Option<usize>,
+ #[proptest(strategy = "arbitrary_size_change()")]
+ 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>,
+ },
+ ToggleWindowFloating {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ id: Option<usize>,
+ },
+ SetWindowFloating {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ id: Option<usize>,
+ floating: bool,
+ },
+ FocusFloating,
+ FocusTiling,
+ SwitchFocusFloatingTiling,
+ MoveFloatingWindow {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ id: Option<usize>,
+ #[proptest(strategy = "arbitrary_position_change()")]
+ x: PositionChange,
+ #[proptest(strategy = "arbitrary_position_change()")]
+ y: PositionChange,
+ animate: bool,
+ },
+ SetParent {
+ #[proptest(strategy = "1..=5usize")]
+ id: usize,
+ #[proptest(strategy = "prop::option::of(1..=5usize)")]
+ new_parent_id: Option<usize>,
+ },
+ 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),
+ ViewOffsetGestureBegin {
+ #[proptest(strategy = "1..=5usize")]
+ output_idx: usize,
+ is_touchpad: bool,
+ },
+ ViewOffsetGestureUpdate {
+ #[proptest(strategy = "arbitrary_view_offset_gesture_delta()")]
+ delta: f64,
+ timestamp: Duration,
+ is_touchpad: bool,
+ },
+ ViewOffsetGestureEnd {
+ is_touchpad: Option<bool>,
+ },
+ WorkspaceSwitchGestureBegin {
+ #[proptest(strategy = "1..=5usize")]
+ output_idx: usize,
+ is_touchpad: bool,
+ },
+ WorkspaceSwitchGestureUpdate {
+ #[proptest(strategy = "-400f64..400f64")]
+ delta: f64,
+ timestamp: Duration,
+ is_touchpad: bool,
+ },
+ WorkspaceSwitchGestureEnd {
+ cancelled: bool,
+ is_touchpad: Option<bool>,
+ },
+ 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,
+ },
+ 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,
+ },
+}
+
+impl Op {
+ fn apply(self, layout: &mut Layout<TestWindow>) {
+ match self {
+ Op::AddOutput(id) => {
+ let name = format!("output{id}");
+ if layout.outputs().any(|o| o.name() == name) {
+ return;
+ }
+
+ let output = Output::new(
+ name.clone(),
+ 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,
+ None,
+ None,
+ );
+ output.user_data().insert_if_missing(|| OutputName {
+ connector: name,
+ make: None,
+ model: None,
+ serial: None,
+ });
+ layout.add_output(output.clone());
+ }
+ Op::AddScaledOutput { id, scale } => {
+ let name = format!("output{id}");
+ if layout.outputs().any(|o| o.name() == name) {
+ return;
+ }
+
+ let output = Output::new(
+ name.clone(),
+ 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,
+ });
+ layout.add_output(output.clone());
+ }
+ Op::RemoveOutput(id) => {
+ let name = format!("output{id}");
+ let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
+ return;
+ };
+
+ layout.remove_output(&output);
+ }
+ Op::FocusOutput(id) => {
+ let name = format!("output{id}");
+ let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
+ return;
+ };
+
+ layout.focus_output(&output);
+ }
+ 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}"));
+ }
+ Op::SetWorkspaceName {
+ new_ws_name,
+ ws_name,
+ } => {
+ let ws_ref =
+ ws_name.map(|ws_name| WorkspaceReference::Name(format!("ws{ws_name}")));
+ layout.set_workspace_name(format!("ws{new_ws_name}"), ws_ref);
+ }
+ Op::UnsetWorkspaceName { ws_name } => {
+ let ws_ref =
+ ws_name.map(|ws_name| WorkspaceReference::Name(format!("ws{ws_name}")));
+ layout.unset_workspace_name(ws_ref);
+ }
+ Op::AddWindow { mut params } => {
+ if layout.has_window(&params.id) {
+ return;
+ }
+ if let Some(parent_id) = params.parent_id {
+ if parent_id_causes_loop(layout, params.id, parent_id) {
+ params.parent_id = None;
+ }
+ }
+
+ let win = TestWindow::new(params);
+ layout.add_window(
+ win,
+ AddWindowTarget::Auto,
+ None,
+ None,
+ false,
+ params.is_floating,
+ ActivateWindow::default(),
+ );
+ }
+ Op::AddWindowNextTo {
+ mut params,
+ next_to_id,
+ } => {
+ let mut found_next_to = false;
+
+ if let Some(InteractiveMoveState::Moving(move_)) = &layout.interactive_move {
+ let win_id = move_.tile.window().0.id;
+ if win_id == params.id {
+ return;
+ }
+ if win_id == next_to_id {
+ found_next_to = true;
+ }
+ }
+
+ 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 == params.id {
+ return;
+ }
+
+ if win.0.id == next_to_id {
+ found_next_to = true;
+ }
+ }
+ }
+ }
+ }
+ MonitorSet::NoOutputs { workspaces, .. } => {
+ for ws in workspaces {
+ for win in ws.windows() {
+ if win.0.id == params.id {
+ return;
+ }
+
+ if win.0.id == next_to_id {
+ found_next_to = true;
+ }
+ }
+ }
+ }
+ }
+
+ if !found_next_to {
+ return;
+ }
+
+ if let Some(parent_id) = params.parent_id {
+ if parent_id_causes_loop(layout, params.id, parent_id) {
+ params.parent_id = None;
+ }
+ }
+
+ let win = TestWindow::new(params);
+ layout.add_window(
+ win,
+ AddWindowTarget::NextTo(&next_to_id),
+ None,
+ None,
+ false,
+ params.is_floating,
+ ActivateWindow::default(),
+ );
+ }
+ Op::AddWindowToNamedWorkspace {
+ mut params,
+ ws_name,
+ } => {
+ let ws_name = format!("ws{ws_name}");
+ let mut ws_id = None;
+
+ if let Some(InteractiveMoveState::Moving(move_)) = &layout.interactive_move {
+ if move_.tile.window().0.id == params.id {
+ return;
+ }
+ }
+
+ 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 == params.id {
+ return;
+ }
+ }
+
+ if ws
+ .name
+ .as_ref()
+ .is_some_and(|name| name.eq_ignore_ascii_case(&ws_name))
+ {
+ ws_id = Some(ws.id());
+ }
+ }
+ }
+ }
+ MonitorSet::NoOutputs { workspaces, .. } => {
+ for ws in workspaces {
+ for win in ws.windows() {
+ if win.0.id == params.id {
+ return;
+ }
+ }
+
+ if ws
+ .name
+ .as_ref()
+ .is_some_and(|name| name.eq_ignore_ascii_case(&ws_name))
+ {
+ ws_id = Some(ws.id());
+ }
+ }
+ }
+ }
+
+ let Some(ws_id) = ws_id else {
+ return;
+ };
+
+ if let Some(parent_id) = params.parent_id {
+ if parent_id_causes_loop(layout, params.id, parent_id) {
+ params.parent_id = None;
+ }
+ }
+
+ let win = TestWindow::new(params);
+ layout.add_window(
+ win,
+ AddWindowTarget::Workspace(ws_id),
+ None,
+ None,
+ false,
+ params.is_floating,
+ ActivateWindow::default(),
+ );
+ }
+ Op::CloseWindow(id) => {
+ layout.remove_window(&id, Transaction::new());
+ }
+ Op::FullscreenWindow(id) => {
+ layout.toggle_fullscreen(&id);
+ }
+ Op::SetFullscreenWindow {
+ window,
+ is_fullscreen,
+ } => {
+ layout.set_fullscreen(&window, is_fullscreen);
+ }
+ Op::FocusColumnLeft => layout.focus_left(),
+ Op::FocusColumnRight => layout.focus_right(),
+ 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::FocusWindow(id) => layout.activate_window(&id),
+ Op::MoveColumnLeft => layout.move_left(),
+ Op::MoveColumnRight => layout.move_right(),
+ 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::SwapWindowInDirection(direction) => layout.swap_window_in_direction(direction),
+ Op::CenterColumn => layout.center_column(),
+ Op::CenterWindow { id } => {
+ let id = id.filter(|id| layout.has_window(id));
+ layout.center_window(id.as_ref());
+ }
+ Op::FocusWorkspaceDown => layout.switch_workspace_down(),
+ Op::FocusWorkspaceUp => layout.switch_workspace_up(),
+ Op::FocusWorkspace(idx) => layout.switch_workspace(idx),
+ 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.has_window(id));
+ layout.move_to_workspace(window_id.as_ref(), workspace_idx);
+ }
+ 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