From 8665003269d1fbe4efe3c477a71400392930cac9 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Sat, 30 Nov 2024 09:18:33 +0300 Subject: layout: Extract ScrollingSpace Leave the Workspace to do the workspace parts, and extract the scrolling parts into a new file. This is a pre-requisite for things like the floating layer (which will live in a workspace alongside the scrolling layer). As part of this huge refactor, I found and fixed at least these issues: - Wrong horizontal popup unconstraining for a smaller window in an always-centered column. - Wrong workspace switch in focus_up_or_right(). --- src/handlers/compositor.rs | 9 +- src/handlers/xdg_shell.rs | 25 +- src/input/mod.rs | 8 +- src/layout/mod.rs | 323 +--- src/layout/monitor.rs | 232 +-- src/layout/scrolling.rs | 3985 +++++++++++++++++++++++++++++++++++++++++ src/layout/workspace.rs | 4216 +++++--------------------------------------- src/window/mod.rs | 2 +- src/window/unmapped.rs | 2 +- 9 files changed, 4588 insertions(+), 4214 deletions(-) create mode 100644 src/layout/scrolling.rs (limited to 'src') diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index ae53bfc8..fe9f8839 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -189,9 +189,8 @@ impl CompositorHandler for State { if let Some(output) = output.cloned() { self.niri.layout.start_open_animation_for_window(&window); - let new_active_window = - self.niri.layout.active_window().map(|(m, _)| &m.window); - if new_active_window == Some(&window) { + let new_focus = self.niri.layout.focus().map(|m| &m.window); + if new_focus == Some(&window) { self.maybe_warp_cursor_to_focus(); } @@ -242,7 +241,7 @@ impl CompositorHandler for State { // The toplevel got unmapped. // // Test client: wleird-unmap. - let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window); + let active_window = self.niri.layout.focus().map(|m| &m.window); let was_active = active_window == Some(&window); #[cfg(feature = "xdp-gnome-screencast")] @@ -290,7 +289,7 @@ impl CompositorHandler for State { self.niri.layout.update_window(&window, serial); // Popup placement depends on window size which might have changed. - self.update_reactive_popups(&window, &output); + self.update_reactive_popups(&window); self.niri.queue_redraw(&output); return; diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 834bbf64..ae860e4a 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -42,7 +42,7 @@ use crate::input::resize_grab::ResizeGrab; use crate::input::touch_move_grab::TouchMoveGrab; use crate::input::touch_resize_grab::TouchResizeGrab; use crate::input::{PointerOrTouchStartData, DOUBLE_CLICK_TIME}; -use crate::layout::workspace::ColumnWidth; +use crate::layout::scrolling::ColumnWidth; use crate::niri::{PopupGrabState, State}; use crate::utils::transaction::Transaction; use crate::utils::{get_monotonic_time, output_matches_name, send_scale_transform, ResizeEdge}; @@ -609,7 +609,7 @@ impl XdgShellHandler for State { .start_close_animation_for_window(renderer, &window, blocker); }); - let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window); + let active_window = self.niri.layout.focus().map(|m| &m.window); let was_active = active_window == Some(&window); self.niri.layout.remove_window(&window, transaction.clone()); @@ -928,8 +928,8 @@ impl State { }; // Figure out if the root is a window or a layer surface. - if let Some((mapped, output)) = self.niri.layout.find_window_and_output(&root) { - self.unconstrain_window_popup(popup, &mapped.window, output); + if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&root) { + self.unconstrain_window_popup(popup, &mapped.window); } else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| { let map = layer_map_for_output(o); let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?; @@ -939,19 +939,10 @@ impl State { } } - fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window, output: &Output) { - let window_geo = window.geometry(); - let output_geo = self.niri.global_space.output_geometry(output).unwrap(); - + fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window) { // The target geometry for the positioner should be relative to its parent's geometry, so // we will compute that here. - // - // We try to keep regular window popups within the window itself horizontally (since the - // window can be scrolled to both edges of the screen), but within the whole monitor's - // height. - let mut target = - Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h)).to_f64(); - target.loc -= self.niri.layout.window_loc(window).unwrap(); + let mut target = self.niri.layout.popup_target_rect(window); target.loc -= get_popup_toplevel_coords(popup).to_f64(); self.position_popup_within_rect(popup, target); @@ -1016,7 +1007,7 @@ impl State { } } - pub fn update_reactive_popups(&self, window: &Window, output: &Output) { + pub fn update_reactive_popups(&self, window: &Window) { let _span = tracy_client::span!("Niri::update_reactive_popups"); for (popup, _) in PopupManager::popups_for_surface( @@ -1025,7 +1016,7 @@ impl State { match &popup { xdg_popup @ PopupKind::Xdg(popup) => { if popup.with_pending_state(|state| state.positioner.reactive) { - self.unconstrain_window_popup(xdg_popup, window, output); + self.unconstrain_window_popup(xdg_popup, window); if let Err(err) = popup.send_pending_configure() { warn!("error re-configuring reactive popup: {err:?}"); } diff --git a/src/input/mod.rs b/src/input/mod.rs index 545c82d7..13ed6d7f 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -581,8 +581,8 @@ impl State { self.open_screenshot_ui(); } Action::ScreenshotWindow => { - let active = self.niri.layout.active_window(); - if let Some((mapped, output)) = active { + let focus = self.niri.layout.focus_with_output(); + if let Some((mapped, output)) = focus { self.backend.with_primary_renderer(|renderer| { if let Err(err) = self.niri.screenshot_window(renderer, output, mapped) { warn!("error taking screenshot: {err:?}"); @@ -990,8 +990,8 @@ impl State { self.niri.layout.move_to_workspace(Some(&window), index); // If we focused the target window. - let new_active_win = self.niri.layout.active_window(); - if new_active_win.map_or(false, |(win, _)| win.window == window) { + let new_focus = self.niri.layout.focus(); + if new_focus.map_or(false, |win| win.window == window) { self.maybe_warp_cursor_to_focus(); } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 6cb4dd44..53248d75 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -40,6 +40,7 @@ use niri_config::{ Workspace as WorkspaceConfig, }; use niri_ipc::SizeChange; +use scrolling::{Column, ColumnWidth, InsertHint, InsertPosition}; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; use smithay::backend::renderer::element::Id; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; @@ -51,9 +52,8 @@ use workspace::WorkspaceId; pub use self::monitor::MonitorRenderElement; use self::monitor::{Monitor, WorkspaceSwitch}; -use self::workspace::{compute_working_area, Column, ColumnWidth, InsertHint, OutputId, Workspace}; +use self::workspace::{OutputId, Workspace}; use crate::animation::Clock; -use crate::layout::workspace::InsertPosition; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::snapshot::RenderSnapshot; @@ -70,6 +70,7 @@ pub mod focus_ring; pub mod insert_hint_element; pub mod monitor; pub mod opening_window; +pub mod scrolling; pub mod tile; pub mod workspace; @@ -801,10 +802,7 @@ impl Layout { let activate = activate.map_smart(|| { // Don't steal focus from an active fullscreen window. let ws = &mon.workspaces[ws_idx]; - if mon_idx == *active_monitor_idx - && !ws.columns.is_empty() - && ws.columns[ws.active_column_idx].is_fullscreen - { + if mon_idx == *active_monitor_idx && ws.is_active_fullscreen() { return false; } @@ -829,7 +827,7 @@ impl Layout { }) .unwrap(); let activate = activate.map_smart(|| true); - ws.add_window(None, window, activate, width, is_full_width); + ws.add_window(window, activate, width, is_full_width); None } } @@ -881,7 +879,7 @@ impl Layout { let activate = activate.map_smart(|| { // Don't steal focus from an active fullscreen window. let ws = &mon.workspaces[mon.active_workspace_idx]; - ws.columns.is_empty() || !ws.columns[ws.active_column_idx].is_fullscreen + !ws.is_active_fullscreen() }); mon.add_window( @@ -904,7 +902,7 @@ impl Layout { &mut workspaces[0] }; let activate = activate.map_smart(|| true); - ws.add_window(None, window, activate, width, is_full_width); + ws.add_window(window, activate, width, is_full_width); None } } @@ -991,13 +989,7 @@ impl Layout { let activate = activate.map_smart(|| { // Don't steal focus from an active fullscreen window. let ws = &mon.workspaces[mon.active_workspace_idx]; - if mon_idx == *active_monitor_idx - && !ws.columns.is_empty() - && ws.columns[ws.active_column_idx].is_fullscreen - { - return false; - } - true + mon_idx != *active_monitor_idx || !ws.is_active_fullscreen() }); mon.add_window( @@ -1263,37 +1255,28 @@ impl Layout { None } - pub fn window_loc(&self, window: &W::Id) -> Option> { + /// 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 { if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { if move_.tile.window().id() == window { - return Some(move_.tile.window_loc()); - } - } - - match &self.monitor_set { - MonitorSet::Normal { monitors, .. } => { - for mon in monitors { - for ws in &mon.workspaces { - for col in &ws.columns { - if let Some(idx) = col.position(window) { - return Some(col.window_loc(idx)); - } - } - } - } - } - MonitorSet::NoOutputs { workspaces, .. } => { - for ws in workspaces { - for col in &ws.columns { - if let Some(idx) = col.position(window) { - return Some(col.window_loc(idx)); - } - } - } + // 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; } } - None + self.workspaces() + .find_map(|(_, _, ws)| ws.popup_target_rect(window)) + .unwrap() } pub fn update_output_size(&mut self, output: &Output) { @@ -1305,13 +1288,8 @@ impl Layout { for mon in monitors { if &mon.output == output { - let scale = output.current_scale(); - let transform = output.current_transform(); - let view_size = output_size(output); - let working_area = compute_working_area(output, self.options.struts); - for ws in &mut mon.workspaces { - ws.set_view_size(scale, transform, view_size, working_area); + ws.update_output_size(); } break; @@ -1471,31 +1449,6 @@ impl Layout { Some(&mut mon.workspaces[mon.active_workspace_idx]) } - pub fn active_window(&self) -> Option<(&W, &Output)> { - if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { - return Some((move_.tile.window(), &move_.output)); - } - - let MonitorSet::Normal { - monitors, - active_monitor_idx, - .. - } = &self.monitor_set - else { - return None; - }; - - let mon = &monitors[*active_monitor_idx]; - let ws = &mon.workspaces[mon.active_workspace_idx]; - - if ws.columns.is_empty() { - return None; - } - - let col = &ws.columns[ws.active_column_idx]; - Some((col.tiles[col.active_tile_idx].window(), &mon.output)) - } - pub fn windows_for_output(&self, output: &Output) -> impl Iterator + '_ { let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { panic!() @@ -1661,11 +1614,7 @@ impl Layout { pub fn move_column_left_or_to_output(&mut self, output: &Output) -> bool { if let Some(monitor) = self.active_monitor() { - let workspace = monitor.active_workspace(); - let curr_idx = workspace.active_column_idx; - - if !workspace.columns.is_empty() && curr_idx != 0 { - monitor.move_left(); + if monitor.move_left() { return false; } } @@ -1676,11 +1625,7 @@ impl Layout { pub fn move_column_right_or_to_output(&mut self, output: &Output) -> bool { if let Some(monitor) = self.active_monitor() { - let workspace = monitor.active_workspace(); - let curr_idx = workspace.active_column_idx; - - if !workspace.columns.is_empty() && curr_idx != workspace.columns.len() - 1 { - monitor.move_right(); + if monitor.move_right() { return false; } } @@ -1807,15 +1752,8 @@ impl Layout { pub fn focus_window_up_or_output(&mut self, output: &Output) -> bool { if let Some(monitor) = self.active_monitor() { - let workspace = monitor.active_workspace(); - - if !workspace.columns.is_empty() { - let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx; - let new_idx = curr_idx.saturating_sub(1); - if curr_idx != new_idx { - workspace.focus_up(); - return false; - } + if monitor.focus_up() { + return false; } } @@ -1825,16 +1763,8 @@ impl Layout { pub fn focus_window_down_or_output(&mut self, output: &Output) -> bool { if let Some(monitor) = self.active_monitor() { - let workspace = monitor.active_workspace(); - - if !workspace.columns.is_empty() { - let column = &workspace.columns[workspace.active_column_idx]; - let curr_idx = column.active_tile_idx; - let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1); - if curr_idx != new_idx { - workspace.focus_down(); - return false; - } + if monitor.focus_down() { + return false; } } @@ -1844,11 +1774,7 @@ impl Layout { pub fn focus_column_left_or_output(&mut self, output: &Output) -> bool { if let Some(monitor) = self.active_monitor() { - let workspace = monitor.active_workspace(); - let curr_idx = workspace.active_column_idx; - - if !workspace.columns.is_empty() && curr_idx != 0 { - monitor.focus_left(); + if monitor.focus_left() { return false; } } @@ -1859,12 +1785,7 @@ impl Layout { pub fn focus_column_right_or_output(&mut self, output: &Output) -> bool { if let Some(monitor) = self.active_monitor() { - let workspace = monitor.active_workspace(); - let curr_idx = workspace.active_column_idx; - let columns = &workspace.columns; - - if !workspace.columns.is_empty() && curr_idx != columns.len() - 1 { - monitor.focus_right(); + if monitor.focus_right() { return false; } } @@ -2040,8 +1961,12 @@ impl Layout { } pub fn focus(&self) -> Option<&W> { + self.focus_with_output().map(|(win, _out)| win) + } + + pub fn focus_with_output(&self) -> Option<(&W, &Output)> { if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { - return Some(move_.tile.window()); + return Some((move_.tile.window(), &move_.output)); } let MonitorSet::Normal { @@ -2053,7 +1978,8 @@ impl Layout { return None; }; - monitors[*active_monitor_idx].focus() + let mon = &monitors[*active_monitor_idx]; + mon.active_window().map(|win| (win, &mon.output)) } /// Returns the window under the cursor and the position of its toplevel surface within the @@ -2248,12 +2174,12 @@ impl Layout { } assert!( - monitor.workspaces.last().unwrap().columns.is_empty(), + !monitor.workspaces.last().unwrap().has_windows(), "monitor must have an empty workspace in the end" ); if monitor.options.empty_workspace_above_first { assert!( - monitor.workspaces.first().unwrap().columns.is_empty(), + !monitor.workspaces.first().unwrap().has_windows(), "first workspace must be empty when empty_workspace_above_first is set" ) } @@ -2710,34 +2636,21 @@ impl Layout { .position(|mon| &mon.output == output) .unwrap(); - let (mon_idx, ws_idx, col_idx, tile_idx) = if let Some(window) = window { + let (mon_idx, ws_idx) = if let Some(window) = window { monitors .iter() .enumerate() .find_map(|(mon_idx, mon)| { - mon.workspaces.iter().enumerate().find_map(|(ws_idx, ws)| { - ws.columns.iter().enumerate().find_map(|(col_idx, col)| { - col.tiles - .iter() - .position(|tile| tile.window().id() == window) - .map(|tile_idx| (mon_idx, ws_idx, col_idx, tile_idx)) - }) - }) + 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]; - let ws_idx = mon.active_workspace_idx; - let ws = &mon.workspaces[ws_idx]; - - if ws.columns.is_empty() { - return; - } - - let col_idx = ws.active_column_idx; - let tile_idx = ws.columns[col_idx].active_tile_idx; - (mon_idx, ws_idx, col_idx, tile_idx) + (mon_idx, mon.active_workspace_idx) }; let workspace_idx = target_ws_idx.unwrap_or(monitors[new_idx].active_workspace_idx); @@ -2746,14 +2659,20 @@ impl Layout { } let mon = &mut monitors[mon_idx]; - let ws = &mut mon.workspaces[ws_idx]; - let column = &ws.columns[col_idx]; - let activate = mon_idx == *active_monitor_idx - && ws_idx == mon.active_workspace_idx - && col_idx == ws.active_column_idx - && tile_idx == column.active_tile_idx; + let activate = window.map_or(true, |win| { + mon_idx == *active_monitor_idx + && mon.active_window().map(|win| win.id()) == Some(win) + }); - let removed = ws.remove_tile_by_idx(col_idx, tile_idx, Transaction::new(), None); + 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; + }; self.add_window_by_idx( new_idx, @@ -2788,10 +2707,9 @@ impl Layout { let current = &mut monitors[*active_monitor_idx]; let ws = current.active_workspace(); - if !ws.has_windows() { + let Some(column) = ws.remove_active_column() else { return; - } - let column = ws.remove_column_by_idx(ws.active_column_idx, None); + }; let workspace_idx = monitors[new_idx].active_workspace_idx; self.add_column_by_idx(new_idx, workspace_idx, column, true); @@ -3373,14 +3291,7 @@ impl Layout { }; // No point in trying to use the pointer position without outputs. - ws.add_tile( - None, - move_.tile, - true, - move_.width, - move_.is_full_width, - None, - ); + ws.add_tile(None, move_.tile, true, move_.width, move_.is_full_width); } } } @@ -3491,31 +3402,11 @@ impl Layout { } } - match &mut self.monitor_set { - MonitorSet::Normal { monitors, .. } => { - for mon in monitors { - for ws in &mut mon.workspaces { - for col in &mut ws.columns { - for tile in &mut col.tiles { - if tile.window().id() == window { - tile.start_open_animation(); - return; - } - } - } - } - } - } - MonitorSet::NoOutputs { workspaces, .. } => { - for ws in workspaces { - for col in &mut ws.columns { - for tile in &mut col.tiles { - if tile.window().id() == window { - tile.start_open_animation(); - return; - } - } - } + for ws in self.workspaces_mut() { + for tile in ws.tiles_mut() { + if tile.window().id() == window { + tile.start_open_animation(); + return; } } } @@ -3615,7 +3506,7 @@ impl Layout { .find(|ws| ws.id() == ws_id) .unwrap(); - let tile_pos = tile_pos + Point::from((ws.view_pos(), 0.)) - offset; + let tile_pos = tile_pos - offset; ws.start_close_animation_for_tile(renderer, snapshot, tile_size, tile_pos, blocker); return; } @@ -3823,7 +3714,6 @@ mod tests { use smithay::utils::Rectangle; use super::*; - use crate::utils::round_logical_in_physical; impl Default for Layout { fn default() -> Self { @@ -5532,7 +5422,8 @@ mod tests { "the second workspace must remain active" ); assert_eq!( - mon.workspaces[0].active_column_idx, 1, + mon.workspaces[0].scrolling().active_column_idx(), + 1, "the new window must become active" ); } @@ -5577,7 +5468,8 @@ mod tests { "the second workspace must remain active" ); assert_eq!( - mon.workspaces[1].active_column_idx, 1, + mon.workspaces[1].scrolling().active_column_idx(), + 1, "the new window must become active" ); } @@ -5768,71 +5660,6 @@ mod tests { layout.verify_invariants(); } - #[test] - fn working_area_starts_at_physical_pixel() { - let struts = Struts { - left: FloatOrInt(0.5), - right: FloatOrInt(1.), - top: FloatOrInt(0.75), - bottom: FloatOrInt(1.), - }; - - let output = Output::new( - String::from("output"), - 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, - ); - - let area = compute_working_area(&output, struts); - - assert_eq!(round_logical_in_physical(1., area.loc.x), area.loc.x); - assert_eq!(round_logical_in_physical(1., area.loc.y), area.loc.y); - } - - #[test] - fn large_fractional_strut() { - let struts = Struts { - left: FloatOrInt(0.), - right: FloatOrInt(0.), - top: FloatOrInt(50000.5), - bottom: FloatOrInt(0.), - }; - - let output = Output::new( - String::from("output"), - 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, - ); - - compute_working_area(&output, struts); - } - #[test] fn set_window_height_recomputes_to_auto() { let ops = [ diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index 1ecd812c..67b09e20 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -9,11 +9,9 @@ use smithay::backend::renderer::element::utils::{ use smithay::output::Output; use smithay::utils::{Logical, Point, Rectangle}; +use super::scrolling::{Column, ColumnWidth}; use super::tile::Tile; -use super::workspace::{ - compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceId, - WorkspaceRenderElement, -}; +use super::workspace::{OutputId, Workspace, WorkspaceId, WorkspaceRenderElement}; use super::{LayoutElement, Options}; use crate::animation::{Animation, Clock}; use crate::input::swipe_tracker::SwipeTracker; @@ -230,7 +228,7 @@ impl Monitor { ) { let workspace = &mut self.workspaces[workspace_idx]; - workspace.add_window(None, window, activate, width, is_full_width); + workspace.add_window(window, activate, width, is_full_width); // After adding a new window, workspace becomes this output's own. workspace.original_output = OutputId::new(&self.output); @@ -275,7 +273,7 @@ impl Monitor { pub fn add_column(&mut self, mut workspace_idx: usize, column: Column, activate: bool) { let workspace = &mut self.workspaces[workspace_idx]; - workspace.add_column(None, column, activate, None); + workspace.add_column(column, activate); // After adding a new window, workspace becomes this output's own. workspace.original_output = OutputId::new(&self.output); @@ -304,7 +302,7 @@ impl Monitor { ) { let workspace = &mut self.workspaces[workspace_idx]; - workspace.add_tile(column_idx, tile, activate, width, is_full_width, None); + workspace.add_tile(column_idx, tile, activate, width, is_full_width); // After adding a new window, workspace becomes this output's own. workspace.original_output = OutputId::new(&self.output); @@ -392,12 +390,12 @@ impl Monitor { false } - pub fn move_left(&mut self) { - self.active_workspace().move_left(); + pub fn move_left(&mut self) -> bool { + self.active_workspace().move_left() } - pub fn move_right(&mut self) { - self.active_workspace().move_right(); + pub fn move_right(&mut self) -> bool { + self.active_workspace().move_right() } pub fn move_column_to_first(&mut self) { @@ -417,40 +415,23 @@ impl Monitor { } pub fn move_down_or_to_workspace_down(&mut self) { - let workspace = self.active_workspace(); - if workspace.columns.is_empty() { - return; - } - let column = &mut workspace.columns[workspace.active_column_idx]; - let curr_idx = column.active_tile_idx; - let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1); - if curr_idx == new_idx { + if !self.active_workspace().move_down() { self.move_to_workspace_down(); - } else { - workspace.move_down(); } } pub fn move_up_or_to_workspace_up(&mut self) { - let workspace = self.active_workspace(); - if workspace.columns.is_empty() { - return; - } - let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx; - let new_idx = curr_idx.saturating_sub(1); - if curr_idx == new_idx { + if !self.active_workspace().move_up() { self.move_to_workspace_up(); - } else { - workspace.move_up(); } } - pub fn focus_left(&mut self) { - self.active_workspace().focus_left(); + pub fn focus_left(&mut self) -> bool { + self.active_workspace().focus_left() } - pub fn focus_right(&mut self) { - self.active_workspace().focus_right(); + pub fn focus_right(&mut self) -> bool { + self.active_workspace().focus_right() } pub fn focus_column_first(&mut self) { @@ -469,98 +450,39 @@ impl Monitor { self.active_workspace().focus_column_left_or_last(); } - pub fn focus_down(&mut self) { - self.active_workspace().focus_down(); + pub fn focus_down(&mut self) -> bool { + self.active_workspace().focus_down() } - pub fn focus_up(&mut self) { - self.active_workspace().focus_up(); + pub fn focus_up(&mut self) -> bool { + self.active_workspace().focus_up() } pub fn focus_down_or_left(&mut self) { - let workspace = self.active_workspace(); - if !workspace.columns.is_empty() { - let column = &workspace.columns[workspace.active_column_idx]; - let curr_idx = column.active_tile_idx; - let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1); - if curr_idx == new_idx { - self.focus_left(); - } else { - workspace.focus_down(); - } - } + self.active_workspace().focus_down_or_left(); } pub fn focus_down_or_right(&mut self) { - let workspace = self.active_workspace(); - if !workspace.columns.is_empty() { - let column = &workspace.columns[workspace.active_column_idx]; - let curr_idx = column.active_tile_idx; - let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1); - if curr_idx == new_idx { - self.focus_right(); - } else { - workspace.focus_down(); - } - } + self.active_workspace().focus_down_or_right(); } pub fn focus_up_or_left(&mut self) { - let workspace = self.active_workspace(); - if !workspace.columns.is_empty() { - let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx; - let new_idx = curr_idx.saturating_sub(1); - if curr_idx == new_idx { - self.focus_left(); - } else { - workspace.focus_up(); - } - } + self.active_workspace().focus_up_or_left(); } pub fn focus_up_or_right(&mut self) { - let workspace = self.active_workspace(); - if workspace.columns.is_empty() { - self.switch_workspace_up(); - } else { - let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx; - let new_idx = curr_idx.saturating_sub(1); - if curr_idx == new_idx { - self.focus_right(); - } else { - workspace.focus_up(); - } - } + self.active_workspace().focus_up_or_right(); } pub fn focus_window_or_workspace_down(&mut self) { - let workspace = self.active_workspace(); - if workspace.columns.is_empty() { + if !self.active_workspace().focus_down() { self.switch_workspace_down(); - } else { - let column = &workspace.columns[workspace.active_column_idx]; - let curr_idx = column.active_tile_idx; - let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1); - if curr_idx == new_idx { - self.switch_workspace_down(); - } else { - workspace.focus_down(); - } } } pub fn focus_window_or_workspace_up(&mut self) { - let workspace = self.active_workspace(); - if workspace.columns.is_empty() { + if !self.active_workspace().focus_up() { self.switch_workspace_up(); - } else { - let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx; - let new_idx = curr_idx.saturating_sub(1); - if curr_idx == new_idx { - self.switch_workspace_up(); - } else { - workspace.focus_up(); - } } } @@ -573,17 +495,9 @@ impl Monitor { } let workspace = &mut self.workspaces[source_workspace_idx]; - if workspace.columns.is_empty() { + let Some(removed) = workspace.remove_active_tile(Transaction::new()) else { return; - } - - let column = &workspace.columns[workspace.active_column_idx]; - let removed = workspace.remove_tile_by_idx( - workspace.active_column_idx, - column.active_tile_idx, - Transaction::new(), - None, - ); + }; self.add_window( new_idx, @@ -603,17 +517,9 @@ impl Monitor { } let workspace = &mut self.workspaces[source_workspace_idx]; - if workspace.columns.is_empty() { + let Some(removed) = workspace.remove_active_tile(Transaction::new()) else { return; - } - - let column = &workspace.columns[workspace.active_column_idx]; - let removed = workspace.remove_tile_by_idx( - workspace.active_column_idx, - column.active_tile_idx, - Transaction::new(), - None, - ); + }; self.add_window( new_idx, @@ -625,30 +531,13 @@ impl Monitor { } pub fn move_to_workspace(&mut self, window: Option<&W::Id>, idx: usize) { - let (source_workspace_idx, col_idx, tile_idx) = if let Some(window) = window { + let source_workspace_idx = if let Some(window) = window { self.workspaces .iter() - .enumerate() - .find_map(|(ws_idx, ws)| { - ws.columns.iter().enumerate().find_map(|(col_idx, col)| { - col.tiles - .iter() - .position(|tile| tile.window().id() == window) - .map(|tile_idx| (ws_idx, col_idx, tile_idx)) - }) - }) + .position(|ws| ws.has_window(window)) .unwrap() } else { - let ws_idx = self.active_workspace_idx; - - let ws = &self.workspaces[ws_idx]; - if ws.columns.is_empty() { - return; - } - - let col_idx = ws.active_column_idx; - let tile_idx = ws.columns[col_idx].active_tile_idx; - (ws_idx, col_idx, tile_idx) + self.active_workspace_idx }; let new_idx = min(idx, self.workspaces.len() - 1); @@ -656,13 +545,19 @@ impl Monitor { return; } - let workspace = &mut self.workspaces[source_workspace_idx]; - let column = &workspace.columns[col_idx]; - let activate = source_workspace_idx == self.active_workspace_idx - && col_idx == workspace.active_column_idx - && tile_idx == column.active_tile_idx; + let activate = window.map_or(true, |win| { + self.active_window().map(|win| win.id()) == Some(win) + }); - let removed = workspace.remove_tile_by_idx(col_idx, tile_idx, Transaction::new(), None); + let workspace = &mut self.workspaces[source_workspace_idx]; + let transaction = Transaction::new(); + let removed = if let Some(window) = window { + workspace.remove_tile(window, transaction) + } else if let Some(removed) = workspace.remove_active_tile(transaction) { + removed + } else { + return; + }; self.add_window( new_idx, @@ -686,11 +581,10 @@ impl Monitor { } let workspace = &mut self.workspaces[source_workspace_idx]; - if workspace.columns.is_empty() { + let Some(column) = workspace.remove_active_column() else { return; - } + }; - let column = workspace.remove_column_by_idx(workspace.active_column_idx, None); self.add_column(new_idx, column, true); } @@ -703,11 +597,10 @@ impl Monitor { } let workspace = &mut self.workspaces[source_workspace_idx]; - if workspace.columns.is_empty() { + let Some(column) = workspace.remove_active_column() else { return; - } + }; - let column = workspace.remove_column_by_idx(workspace.active_column_idx, None); self.add_column(new_idx, column, true); } @@ -720,11 +613,10 @@ impl Monitor { } let workspace = &mut self.workspaces[source_workspace_idx]; - if workspace.columns.is_empty() { + let Some(column) = workspace.remove_active_column() else { return; - } + }; - let column = workspace.remove_column_by_idx(workspace.active_column_idx, None); self.add_column(new_idx, column, true); } @@ -778,14 +670,12 @@ impl Monitor { self.active_workspace().center_column(); } - pub fn focus(&self) -> Option<&W> { - let workspace = &self.workspaces[self.active_workspace_idx]; - if !workspace.has_windows() { - return None; - } + pub fn active_window(&self) -> Option<&W> { + self.active_workspace_ref().active_window() + } - let column = &workspace.columns[workspace.active_column_idx]; - Some(column.tiles[column.active_tile_idx].window()) + pub fn is_active_fullscreen(&self) -> bool { + self.active_workspace_ref().is_active_fullscreen() } pub fn advance_animations(&mut self) { @@ -861,17 +751,6 @@ impl Monitor { ws.update_config(options.clone()); } - if self.options.struts != options.struts { - let scale = self.output.current_scale(); - let transform = self.output.current_transform(); - let view_size = output_size(&self.output); - let working_area = compute_working_area(&self.output, options.struts); - - for ws in &mut self.workspaces { - ws.set_view_size(scale, transform, view_size, working_area); - } - } - self.options = options; } @@ -1070,7 +949,6 @@ impl Monitor { self.workspaces_with_render_positions() .flat_map(move |(ws, offset)| { ws.render_elements(renderer, target) - .into_iter() .filter_map(move |elem| { CropRenderElement::from_element(elem, scale, crop_bounds) }) diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs new file mode 100644 index 00000000..cd75fb06 --- /dev/null +++ b/src/layout/scrolling.rs @@ -0,0 +1,3985 @@ +use std::cmp::{max, min}; +use std::iter::{self, zip}; +use std::rc::Rc; +use std::time::Duration; + +use niri_config::{CenterFocusedColumn, CornerRadius, PresetSize, Struts}; +use niri_ipc::SizeChange; +use ordered_float::NotNan; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size}; + +use super::closing_window::{ClosingWindow, ClosingWindowRenderElement}; +use super::insert_hint_element::{InsertHintElement, InsertHintRenderElement}; +use super::tile::{Tile, TileRenderElement, TileRenderSnapshot}; +use super::workspace::{InteractiveResize, ResolvedSize}; +use super::{ConfigureIntent, InteractiveResizeData, LayoutElement, Options, RemovedTile}; +use crate::animation::{Animation, Clock}; +use crate::input::swipe_tracker::SwipeTracker; +use crate::niri_render_elements; +use crate::render_helpers::renderer::NiriRenderer; +use crate::render_helpers::RenderTarget; +use crate::utils::transaction::{Transaction, TransactionBlocker}; +use crate::utils::ResizeEdge; +use crate::window::ResolvedWindowRules; + +/// Amount of touchpad movement to scroll the view for the width of one working area. +const VIEW_GESTURE_WORKING_AREA_MOVEMENT: f64 = 1200.; + +/// A scrollable-tiling space for windows. +#[derive(Debug)] +pub struct ScrollingSpace { + /// Columns of windows on this space. + columns: Vec>, + + /// Extra per-column data. + data: Vec, + + /// Index of the currently active column, if any. + active_column_idx: usize, + + /// Ongoing interactive resize. + interactive_resize: Option>, + + /// Offset of the view computed from the active column. + /// + /// Any gaps, including left padding from work area left exclusive zone, is handled + /// with this view offset (rather than added as a constant elsewhere in the code). This allows + /// for natural handling of fullscreen windows, which must ignore work area padding. + view_offset: ViewOffset, + + /// Whether to activate the previous, rather than the next, column upon column removal. + /// + /// When a new column is created and removed with no focus changes in-between, it is more + /// natural to activate the previously-focused column. This variable tracks that. + /// + /// Since we only create-and-activate columns immediately to the right of the active column (in + /// contrast to tabs in Firefox, for example), we can track this as a bool, rather than an + /// index of the previous column to activate. + /// + /// The value is the view offset that the previous column had before, to restore it. + activate_prev_column_on_removal: Option, + + /// View offset to restore after unfullscreening. + view_offset_before_fullscreen: Option, + + /// Windows in the closing animation. + closing_windows: Vec, + + /// Indication where an interactively-moved window is about to be placed. + insert_hint: Option, + + /// Insert hint element for rendering. + insert_hint_element: InsertHintElement, + + /// View size for this space. + view_size: Size, + + /// Working area for this space. + /// + /// Takes into account layer-shell exclusive zones and niri struts. + working_area: Rectangle, + + /// Scale of the output the space is on (and rounds its sizes to). + scale: f64, + + /// Clock for driving animations. + clock: Clock, + + /// Configurable properties of the layout. + options: Rc, +} + +niri_render_elements! { + ScrollingSpaceRenderElement => { + Tile = TileRenderElement, + ClosingWindow = ClosingWindowRenderElement, + InsertHint = InsertHintRenderElement, + } +} + +#[derive(Debug, PartialEq)] +pub enum InsertPosition { + NewColumn(usize), + InColumn(usize, usize), +} + +#[derive(Debug)] +pub struct InsertHint { + pub position: InsertPosition, + pub width: ColumnWidth, + pub is_full_width: bool, + pub corner_radius: CornerRadius, +} + +/// Extra per-column data. +#[derive(Debug, Clone, Copy, PartialEq)] +struct ColumnData { + /// Cached actual column width. + width: f64, +} + +#[derive(Debug)] +enum ViewOffset { + /// The view offset is static. + Static(f64), + /// The view offset is animating. + Animation(Animation), + /// The view offset is controlled by the ongoing gesture. + Gesture(ViewGesture), +} + +#[derive(Debug)] +struct ViewGesture { + current_view_offset: f64, + tracker: SwipeTracker, + delta_from_tracker: f64, + // The view offset we'll use if needed for activate_prev_column_on_removal. + stationary_view_offset: f64, + /// Whether the gesture is controlled by the touchpad. + is_touchpad: bool, +} + +#[derive(Debug)] +pub struct Column { + /// Tiles in this column. + /// + /// Must be non-empty. + tiles: Vec>, + + /// Extra per-tile data. + /// + /// Must have the same number of elements as `tiles`. + data: Vec, + + /// Index of the currently active tile. + active_tile_idx: usize, + + /// Desired width of this column. + /// + /// If the column is full-width or full-screened, this is the width that should be restored + /// upon unfullscreening and untoggling full-width. + width: ColumnWidth, + + /// Whether this column is full-width. + is_full_width: bool, + + /// Whether this column contains a single full-screened window. + is_fullscreen: bool, + + /// Animation of the render offset during window swapping. + move_animation: Option, + + /// Latest known view size for this column's workspace. + view_size: Size, + + /// Latest known working area for this column's workspace. + working_area: Rectangle, + + /// Scale of the output the column is on (and rounds its sizes to). + scale: f64, + + /// Clock for driving animations. + clock: Clock, + + /// Configurable properties of the layout. + options: Rc, +} + +/// Extra per-tile data. +#[derive(Debug, Clone, Copy, PartialEq)] +struct TileData { + /// Requested height of the window. + /// + /// This is window height, not tile height, so it excludes tile decorations. + height: WindowHeight, + + /// Cached actual size of the tile. + size: Size, + + /// Cached whether the tile is being interactively resized by its left edge. + interactively_resizing_by_left_edge: bool, +} + +/// Width of a column. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ColumnWidth { + /// Proportion of the current view width. + Proportion(f64), + /// Fixed width in logical pixels. + Fixed(f64), + /// One of the preset widths. + Preset(usize), +} + +/// Height of a window in a column. +/// +/// Every window but one in a column must be `Auto`-sized so that the total height can add up to +/// the workspace height. Resizing a window converts all other windows to `Auto`, weighted to +/// preserve their visual heights at the moment of the conversion. +/// +/// In contrast to column widths, proportional height changes are converted to, and stored as, +/// fixed height right away. With column widths you frequently want e.g. two columns side-by-side +/// with 50% width each, and you want them to remain this way when moving to a differently sized +/// monitor. Windows in a column, however, already auto-size to fill the available height, giving +/// you this behavior. The main reason to set a different window height, then, is when you want +/// something in the window to fit exactly, e.g. to fit 30 lines in a terminal, which corresponds +/// to the `Fixed` variant. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum WindowHeight { + /// Automatically computed *tile* height, distributed across the column according to weights. + /// + /// This controls the tile height rather than the window height because it's easier in the auto + /// height distribution algorithm. + Auto { weight: f64 }, + /// Fixed *window* height in logical pixels. + Fixed(f64), + /// One of the preset heights (tile or window). + Preset(usize), +} + +impl ScrollingSpace { + pub fn new( + view_size: Size, + working_area: Rectangle, + scale: f64, + clock: Clock, + options: Rc, + ) -> Self { + let working_area = compute_working_area(working_area, scale, options.struts); + + Self { + columns: Vec::new(), + data: Vec::new(), + active_column_idx: 0, + interactive_resize: None, + view_offset: ViewOffset::Static(0.), + activate_prev_column_on_removal: None, + view_offset_before_fullscreen: None, + closing_windows: Vec::new(), + insert_hint: None, + insert_hint_element: InsertHintElement::new(options.insert_hint), + view_size, + working_area, + scale, + clock, + options, + } + } + + pub fn update_config( + &mut self, + view_size: Size, + working_area: Rectangle, + scale: f64, + options: Rc, + ) { + let working_area = compute_working_area(working_area, scale, options.struts); + + for (column, data) in zip(&mut self.columns, &mut self.data) { + column.update_config(view_size, working_area, scale, options.clone()); + data.update(column); + } + + self.insert_hint_element.update_config(options.insert_hint); + + self.view_size = view_size; + self.working_area = working_area; + self.scale = scale; + self.options = options; + } + + pub fn update_shaders(&mut self) { + for tile in self.tiles_mut() { + tile.update_shaders(); + } + + self.insert_hint_element.update_shaders(); + } + + pub fn advance_animations(&mut self) { + if let ViewOffset::Animation(anim) = &self.view_offset { + if anim.is_done() { + self.view_offset = ViewOffset::Static(anim.to()); + } + } + + for col in &mut self.columns { + col.advance_animations(); + } + + self.closing_windows.retain_mut(|closing| { + closing.advance_animations(); + closing.are_animations_ongoing() + }); + } + + pub fn are_animations_ongoing(&self) -> bool { + self.view_offset.is_animation() + || self.columns.iter().any(Column::are_animations_ongoing) + || !self.closing_windows.is_empty() + } + + pub fn are_transitions_ongoing(&self) -> bool { + !self.view_offset.is_static() + || self.columns.iter().any(Column::are_animations_ongoing) + || !self.closing_windows.is_empty() + } + + pub fn update_render_elements(&mut self, is_active: bool) { + let view_pos = Point::from((self.view_pos(), 0.)); + let view_size = self.view_size(); + let active_idx = self.active_column_idx; + for (col_idx, (col, col_x)) in self.columns_mut().enumerate() { + let is_active = is_active && col_idx == active_idx; + let col_off = Point::from((col_x, 0.)); + let col_pos = view_pos - col_off - col.render_offset(); + let view_rect = Rectangle::from_loc_and_size(col_pos, view_size); + col.update_render_elements(is_active, view_rect); + } + + if let Some(insert_hint) = &self.insert_hint { + if let Some(area) = self.insert_hint_area(insert_hint) { + let view_rect = Rectangle::from_loc_and_size(area.loc.upscale(-1.), view_size); + self.insert_hint_element.update_render_elements( + area.size, + view_rect, + insert_hint.corner_radius, + self.scale, + ); + } + } + } + + pub fn tiles(&self) -> impl Iterator> + '_ { + self.columns.iter().flat_map(|col| col.tiles.iter()) + } + + pub fn tiles_mut(&mut self) -> impl Iterator> + '_ { + self.columns.iter_mut().flat_map(|col| col.tiles.iter_mut()) + } + + pub fn active_window(&self) -> Option<&W> { + if self.columns.is_empty() { + return None; + } + + let col = &self.columns[self.active_column_idx]; + Some(col.tiles[col.active_tile_idx].window()) + } + + pub fn is_active_fullscreen(&self) -> bool { + if self.columns.is_empty() { + return false; + } + + let col = &self.columns[self.active_column_idx]; + col.is_fullscreen + } + + pub fn toplevel_bounds(&self, rules: &ResolvedWindowRules) -> Size { + let border_config = rules.border.resolve_against(self.options.border); + compute_toplevel_bounds(border_config, self.working_area.size, self.options.gaps) + } + + pub fn new_window_size( + &self, + width: Option, + rules: &ResolvedWindowRules, + ) -> Size { + let border = rules.border.resolve_against(self.options.border); + + let width = if let Some(width) = width { + let is_fixed = matches!(width, ColumnWidth::Fixed(_)); + + let mut width = width.resolve(&self.options, self.working_area.size.w); + + if !is_fixed && !border.off { + width -= border.width.0 * 2.; + } + + max(1, width.floor() as i32) + } else { + 0 + }; + + let mut height = self.working_area.size.h - self.options.gaps * 2.; + if !border.off { + height -= border.width.0 * 2.; + } + + Size::from((width, max(height.floor() as i32, 1))) + } + + pub fn is_centering_focused_column(&self) -> bool { + self.options.center_focused_column == CenterFocusedColumn::Always + || (self.options.always_center_single_column && self.columns.len() <= 1) + } + + fn compute_new_view_offset_fit( + &self, + target_x: Option, + col_x: f64, + width: f64, + is_fullscreen: bool, + ) -> f64 { + if is_fullscreen { + return 0.; + } + + let target_x = target_x.unwrap_or_else(|| self.target_view_pos()); + + let new_offset = compute_new_view_offset( + target_x + self.working_area.loc.x, + self.working_area.size.w, + col_x, + width, + self.options.gaps, + ); + + // Non-fullscreen windows are always offset at least by the working area position. + new_offset - self.working_area.loc.x + } + + fn compute_new_view_offset_centered( + &self, + target_x: Option, + col_x: f64, + width: f64, + is_fullscreen: bool, + ) -> f64 { + if is_fullscreen { + return self.compute_new_view_offset_fit(target_x, col_x, width, is_fullscreen); + } + + // Columns wider than the view are left-aligned (the fit code can deal with that). + if self.working_area.size.w <= width { + return self.compute_new_view_offset_fit(target_x, col_x, width, is_fullscreen); + } + + -(self.working_area.size.w - width) / 2. - self.working_area.loc.x + } + + fn compute_new_view_offset_for_column_fit(&self, target_x: Option, idx: usize) -> f64 { + let col = &self.columns[idx]; + self.compute_new_view_offset_fit( + target_x, + self.column_x(idx), + col.width(), + col.is_fullscreen, + ) + } + + fn compute_new_view_offset_for_column_centered( + &self, + target_x: Option, + idx: usize, + ) -> f64 { + let col = &self.columns[idx]; + self.compute_new_view_offset_centered( + target_x, + self.column_x(idx), + col.width(), + col.is_fullscreen, + ) + } + + fn compute_new_view_offset_for_column( + &self, + target_x: Option, + idx: usize, + prev_idx: Option, + ) -> f64 { + if self.is_centering_focused_column() { + return self.compute_new_view_offset_for_column_centered(target_x, idx); + } + + match self.options.center_focused_column { + CenterFocusedColumn::Always => { + self.compute_new_view_offset_for_column_centered(target_x, idx) + } + CenterFocusedColumn::OnOverflow => { + let Some(prev_idx) = prev_idx else { + return self.compute_new_view_offset_for_column_fit(target_x, idx); + }; + + // Always take the left or right neighbor of the target as the source. + let source_idx = if prev_idx > idx { + min(idx + 1, self.columns.len() - 1) + } else { + idx.saturating_sub(1) + }; + + let source_col_x = self.column_x(source_idx); + let source_col_width = self.columns[source_idx].width(); + + let target_col_x = self.column_x(idx); + let target_col_width = self.columns[idx].width(); + + let total_width = if source_col_x < target_col_x { + // Source is left from target. + target_col_x - source_col_x + target_col_width + } else { + // Source is right from target. + source_col_x - target_col_x + source_col_width + } + self.options.gaps * 2.; + + // If it fits together, do a normal animation, otherwise center the new column. + if total_width <= self.working_area.size.w { + self.compute_new_view_offset_for_column_fit(target_x, idx) + } else { + self.compute_new_view_offset_for_column_centered(target_x, idx) + } + } + CenterFocusedColumn::Never => { + self.compute_new_view_offset_for_column_fit(target_x, idx) + } + } + } + + fn animate_view_offset(&mut self, idx: usize, new_view_offset: f64) { + self.animate_view_offset_with_config( + idx, + new_view_offset, + self.options.animations.horizontal_view_movement.0, + ); + } + + fn animate_view_offset_with_config( + &mut self, + idx: usize, + new_view_offset: f64, + config: niri_config::Animation, + ) { + self.view_offset.cancel_gesture(); + + let new_col_x = self.column_x(idx); + let old_col_x = self.column_x(self.active_column_idx); + let offset_delta = old_col_x - new_col_x; + self.view_offset.offset(offset_delta); + + let pixel = 1. / self.scale; + + // If our view offset is already this or animating towards this, we don't need to do + // anything. + let to_diff = new_view_offset - self.view_offset.target(); + if to_diff.abs() < pixel { + // Correct for any inaccuracy. + self.view_offset.offset(to_diff); + return; + } + + // FIXME: also compute and use current velocity. + self.view_offset = ViewOffset::Animation(Animation::new( + self.clock.clone(), + self.view_offset.current(), + new_view_offset, + 0., + config, + )); + } + + fn animate_view_offset_to_column_centered( + &mut self, + target_x: Option, + idx: usize, + config: niri_config::Animation, + ) { + let new_view_offset = self.compute_new_view_offset_for_column_centered(target_x, idx); + self.animate_view_offset_with_config(idx, new_view_offset, config); + } + + fn animate_view_offset_to_column_with_config( + &mut self, + target_x: Option, + idx: usize, + prev_idx: Option, + config: niri_config::Animation, + ) { + let new_view_offset = self.compute_new_view_offset_for_column(target_x, idx, prev_idx); + self.animate_view_offset_with_config(idx, new_view_offset, config); + } + + fn animate_view_offset_to_column( + &mut self, + target_x: Option, + idx: usize,