diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2025-01-31 17:55:15 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2025-01-31 17:56:43 +0300 |
| commit | 49ddf66c2f77d6dab8bdb84de7345b9c3a28f9df (patch) | |
| tree | f4eb339823bf84b1b70da4c7b51910045dbe6dba /src/layout/tests.rs | |
| parent | a169e0335d2773edd31a1959a3ff4fca994a422a (diff) | |
| download | niri-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.rs | 3163 |
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(¶ms.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 |
