diff options
| author | Rasmus Eneman <rasmus@eneman.eu> | 2024-07-15 15:51:48 +0200 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-10-27 23:07:39 -0700 |
| commit | e887ee93a30390b641bf647d694a1424f7ce4592 (patch) | |
| tree | 94a76c90c2433ad3a0d92015d7ca6ba569ab2979 /src/layout | |
| parent | d640e8515899e552b845cf8f901ebeb126bb12a5 (diff) | |
| download | niri-e887ee93a30390b641bf647d694a1424f7ce4592.tar.gz niri-e887ee93a30390b641bf647d694a1424f7ce4592.tar.bz2 niri-e887ee93a30390b641bf647d694a1424f7ce4592.zip | |
Implement interactive window move
Diffstat (limited to 'src/layout')
| -rw-r--r-- | src/layout/mod.rs | 977 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 53 | ||||
| -rw-r--r-- | src/layout/tile.rs | 13 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 302 |
4 files changed, 1301 insertions, 44 deletions
diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 3e6affb1..b404da16 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -43,19 +43,21 @@ use smithay::backend::renderer::element::Id; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; use smithay::output::{self, Output}; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; -use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform}; -use tile::Tile; +use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size, Transform}; +use tile::{Tile, TileRenderElement}; use workspace::WorkspaceId; pub use self::monitor::MonitorRenderElement; use self::monitor::{Monitor, WorkspaceSwitch}; -use self::workspace::{compute_working_area, Column, ColumnWidth, OutputId, Workspace}; +use self::workspace::{compute_working_area, Column, ColumnWidth, InsertHint, OutputId, Workspace}; +use crate::layout::workspace::InsertPosition; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::snapshot::RenderSnapshot; use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::texture::TextureBuffer; use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements}; +use crate::rubber_band::RubberBand; use crate::utils::transaction::{Transaction, TransactionBlocker}; use crate::utils::{output_matches_name, output_size, round_logical_in_physical_max1, ResizeEdge}; use crate::window::ResolvedWindowRules; @@ -70,6 +72,9 @@ pub mod workspace; /// Size changes up to this many pixels don't animate. pub const RESIZE_ANIMATION_THRESHOLD: f64 = 10.; +/// Pointer needs to move this far to pull a window from the layout. +const INTERACTIVE_MOVE_START_THRESHOLD: f64 = 256. * 256.; + niri_render_elements! { LayoutElementRenderElement<R> => { Wayland = WaylandSurfaceRenderElement<R>, @@ -80,6 +85,41 @@ niri_render_elements! { pub type LayoutElementRenderSnapshot = RenderSnapshot<BakedBuffer<TextureBuffer<GlesTexture>>, BakedBuffer<SolidColorBuffer>>; +#[derive(Debug)] +enum InteractiveMoveState<W: LayoutElement> { + /// Initial rubberbanding; the window remains in the layout. + Starting { + /// The window we're moving. + window_id: W::Id, + /// Current pointer delta from the starting location. + pointer_delta: Point<f64, Logical>, + /// Pointer location within the visual window geometry as ratio from geometry size. + /// + /// This helps the pointer remain inside the window as it resizes. + pointer_ratio_within_window: (f64, f64), + }, + /// Moving; the window is no longer in the layout. + Moving(InteractiveMoveData<W>), +} + +#[derive(Debug)] +struct InteractiveMoveData<W: LayoutElement> { + /// The window being moved. + pub(self) tile: Tile<W>, + /// Output where the window is currently located/rendered. + pub(self) output: Output, + /// Current pointer position within output. + pub(self) pointer_pos_within_output: Point<f64, Logical>, + /// Window column width. + pub(self) width: ColumnWidth, + /// Whether the window column was full-width. + pub(self) is_full_width: bool, + /// Pointer location within the visual window geometry as ratio from geometry size. + /// + /// This helps the pointer remain inside the window as it resizes. + pub(self) pointer_ratio_within_window: (f64, f64), +} + #[derive(Debug, Clone, Copy)] pub struct InteractiveResizeData { pub(self) edges: ResizeEdge, @@ -216,6 +256,8 @@ pub struct Layout<W: LayoutElement> { /// This normally indicates that the layout has keyboard focus, but not always. E.g. when the /// screenshot UI is open, it keeps the layout drawing as active. is_active: bool, + /// Ongoing interactive move. + interactive_move: Option<InteractiveMoveState<W>>, /// Configurable properties of the layout. options: Rc<Options>, } @@ -246,6 +288,7 @@ pub struct Options { pub struts: Struts, pub focus_ring: niri_config::FocusRing, pub border: niri_config::Border, + pub insert_hint: niri_config::InsertHint, pub center_focused_column: CenterFocusedColumn, pub always_center_single_column: bool, /// Column widths that `toggle_width()` switches between. @@ -267,6 +310,7 @@ impl Default for Options { struts: Default::default(), focus_ring: Default::default(), border: Default::default(), + insert_hint: Default::default(), center_focused_column: Default::default(), always_center_single_column: false, preset_column_widths: vec![ @@ -296,6 +340,31 @@ pub struct RemovedTile<W: LayoutElement> { is_full_width: bool, } +impl<W: LayoutElement> InteractiveMoveState<W> { + fn moving(&self) -> Option<&InteractiveMoveData<W>> { + match self { + InteractiveMoveState::Moving(move_) => Some(move_), + _ => None, + } + } +} + +impl<W: LayoutElement> InteractiveMoveData<W> { + fn tile_render_location(&self) -> Point<f64, Logical> { + let scale = Scale::from(self.output.current_scale().fractional_scale()); + let window_size = self.tile.window_size(); + let pointer_offset_within_window = Point::from(( + window_size.w * self.pointer_ratio_within_window.0, + window_size.h * self.pointer_ratio_within_window.1, + )); + let pos = + self.pointer_pos_within_output - pointer_offset_within_window - self.tile.window_loc() + + self.tile.render_offset(); + // Round to physical pixels. + pos.to_physical_precise_round(scale).to_logical(scale) + } +} + impl Options { fn from_config(config: &Config) -> Self { let layout = &config.layout; @@ -329,6 +398,7 @@ impl Options { struts: layout.struts, focus_ring: layout.focus_ring, border: layout.border, + insert_hint: layout.insert_hint, center_focused_column: layout.center_focused_column, always_center_single_column: layout.always_center_single_column, preset_column_widths, @@ -360,6 +430,7 @@ impl<W: LayoutElement> Layout<W> { Self { monitor_set: MonitorSet::NoOutputs { workspaces: vec![] }, is_active: true, + interactive_move: None, options: Rc::new(options), } } @@ -376,6 +447,7 @@ impl<W: LayoutElement> Layout<W> { Self { monitor_set: MonitorSet::NoOutputs { workspaces }, is_active: true, + interactive_move: None, options: opts, } } @@ -693,6 +765,18 @@ impl<W: LayoutElement> Layout<W> { width: Option<ColumnWidth>, is_full_width: bool, ) -> Option<&Output> { + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { + if right_of == move_.tile.window().id() { + let output = move_.output.clone(); + if self.monitor_for_output(&output).is_some() { + self.add_window_on_output(&output, window, width, is_full_width); + return Some(&self.monitor_for_output(&output).unwrap().output); + } else { + return self.add_window(window, width, is_full_width); + } + } + } + let width = self.resolve_default_width(&window, width); match &mut self.monitor_set { @@ -765,6 +849,30 @@ impl<W: LayoutElement> Layout<W> { window: &W::Id, transaction: Transaction, ) -> Option<RemovedTile<W>> { + if let Some(state) = &self.interactive_move { + match state { + InteractiveMoveState::Starting { window_id, .. } => { + if window_id == window { + self.interactive_move_end(window); + } + } + InteractiveMoveState::Moving(move_) => { + if move_.tile.window().id() == window { + let Some(InteractiveMoveState::Moving(move_)) = + self.interactive_move.take() + else { + unreachable!() + }; + return Some(RemovedTile { + tile: move_.tile, + width: move_.width, + is_full_width: move_.is_full_width, + }); + } + } + } + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -811,6 +919,13 @@ impl<W: LayoutElement> Layout<W> { } pub fn update_window(&mut self, window: &W::Id, serial: Option<Serial>) { + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + if move_.tile.window().id() == window { + move_.tile.update_window(); + return; + } + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -834,6 +949,12 @@ impl<W: LayoutElement> Layout<W> { } pub fn find_window_and_output(&self, wl_surface: &WlSurface) -> Option<(&W, &Output)> { + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { + if move_.tile.window().is_wl_surface(wl_surface) { + return Some((move_.tile.window(), &move_.output)); + } + } + if let MonitorSet::Normal { monitors, .. } = &self.monitor_set { for mon in monitors { for ws in &mon.workspaces { @@ -939,6 +1060,12 @@ impl<W: LayoutElement> Layout<W> { &mut self, wl_surface: &WlSurface, ) -> Option<(&mut W, Option<&Output>)> { + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + if move_.tile.window().is_wl_surface(wl_surface) { + return Some((move_.tile.window_mut(), Some(&move_.output))); + } + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -962,6 +1089,12 @@ impl<W: LayoutElement> Layout<W> { } pub fn window_loc(&self, window: &W::Id) -> Option<Point<f64, Logical>> { + 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 { @@ -1012,6 +1145,12 @@ impl<W: LayoutElement> Layout<W> { } pub fn scroll_amount_to_activate(&self, window: &W::Id) -> f64 { + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { + if move_.tile.window().id() == window { + return 0.; + } + } + let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { return 0.; }; @@ -1034,6 +1173,12 @@ impl<W: LayoutElement> Layout<W> { // // This function allows focus-follows-mouse to trigger only on the animation target // workspace. + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { + if move_.tile.window().id() == window { + return true; + } + } + let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { return true; }; @@ -1057,6 +1202,12 @@ impl<W: LayoutElement> Layout<W> { } pub fn activate_window(&mut self, window: &W::Id) { + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { + if move_.tile.window().id() == window { + return; + } + } + let MonitorSet::Normal { monitors, active_monitor_idx, @@ -1146,6 +1297,10 @@ impl<W: LayoutElement> Layout<W> { } 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, @@ -1171,17 +1326,31 @@ impl<W: LayoutElement> Layout<W> { panic!() }; + let moving_window = self + .interactive_move + .as_ref() + .and_then(|x| x.moving()) + .filter(|move_| move_.output == *output) + .map(|move_| move_.tile.window()) + .into_iter(); + let mon = monitors.iter().find(|mon| &mon.output == output).unwrap(); - mon.workspaces.iter().flat_map(|ws| ws.windows()) + let mon_windows = mon.workspaces.iter().flat_map(|ws| ws.windows()); + + moving_window.chain(mon_windows) } - pub fn with_windows(&self, mut f: impl FnMut(&W, Option<&Output>, WorkspaceId)) { + pub fn with_windows(&self, mut f: impl FnMut(&W, Option<&Output>, Option<WorkspaceId>)) { + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { + f(move_.tile.window(), Some(&move_.output), None); + } + match &self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { for ws in &mon.workspaces { for win in ws.windows() { - f(win, Some(&mon.output), ws.id()); + f(win, Some(&mon.output), Some(ws.id())); } } } @@ -1189,7 +1358,7 @@ impl<W: LayoutElement> Layout<W> { MonitorSet::NoOutputs { workspaces } => { for ws in workspaces { for win in ws.windows() { - f(win, None, ws.id()); + f(win, None, Some(ws.id())); } } } @@ -1197,6 +1366,10 @@ impl<W: LayoutElement> Layout<W> { } pub fn with_windows_mut(&mut self, mut f: impl FnMut(&mut W, Option<&Output>)) { + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + f(move_.tile.window_mut(), Some(&move_.output)); + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -1248,7 +1421,15 @@ impl<W: LayoutElement> Layout<W> { return None; }; - monitors.iter().find(|monitor| &monitor.output == output) + monitors.iter().find(|mon| &mon.output == output) + } + + pub fn monitor_for_output_mut(&mut self, output: &Output) -> Option<&mut Monitor<W>> { + let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else { + return None; + }; + + monitors.iter_mut().find(|mon| &mon.output == output) } pub fn monitor_for_workspace(&self, workspace_name: &str) -> Option<&Monitor<W>> { @@ -1362,6 +1543,12 @@ impl<W: LayoutElement> Layout<W> { } pub fn consume_or_expel_window_left(&mut self, window: Option<&W::Id>) { + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + if window == Some(move_.tile.window().id()) { + return; + } + } + let workspace = if let Some(window) = window { Some( self.workspaces_mut() @@ -1379,6 +1566,12 @@ impl<W: LayoutElement> Layout<W> { } pub fn consume_or_expel_window_right(&mut self, window: Option<&W::Id>) { + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + if window == Some(move_.tile.window().id()) { + return; + } + } + let workspace = if let Some(window) = window { Some( self.workspaces_mut() @@ -1576,6 +1769,12 @@ impl<W: LayoutElement> Layout<W> { } pub fn move_to_workspace(&mut self, window: Option<&W::Id>, idx: usize) { + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + if window == Some(move_.tile.window().id()) { + return; + } + } + let Some(monitor) = self.active_monitor() else { return; }; @@ -1666,6 +1865,10 @@ impl<W: LayoutElement> Layout<W> { } pub fn focus(&self) -> Option<&W> { + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { + return Some(move_.tile.window()); + } + let MonitorSet::Normal { monitors, active_monitor_idx, @@ -1694,6 +1897,20 @@ impl<W: LayoutElement> Layout<W> { return None; }; + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { + let tile_pos = move_.tile_render_location(); + let pos_within_tile = pos_within_output - tile_pos; + + if move_.tile.is_in_input_region(pos_within_tile) { + let pos_within_surface = tile_pos + move_.tile.buf_loc(); + return Some((move_.tile.window(), Some(pos_within_surface))); + } else if move_.tile.is_in_activation_region(pos_within_tile) { + return Some((move_.tile.window(), None)); + } + + return None; + }; + let mon = monitors.iter().find(|mon| &mon.output == output)?; mon.window_under(pos_within_output) } @@ -1715,8 +1932,43 @@ impl<W: LayoutElement> Layout<W> { fn verify_invariants(&self) { use std::collections::HashSet; + use approx::assert_abs_diff_eq; + use crate::layout::monitor::WorkspaceSwitch; + let mut move_win_id = None; + if let Some(state) = &self.interactive_move { + match state { + InteractiveMoveState::Starting { + window_id, + pointer_delta: _, + pointer_ratio_within_window: _, + } => { + assert!( + self.has_window(window_id), + "interactive move must be on an existing window" + ); + move_win_id = Some(window_id.clone()); + } + InteractiveMoveState::Moving(move_) => { + let scale = move_.output.current_scale().fractional_scale(); + let options = Options::clone(&self.options).adjusted_for_scale(scale); + assert_eq!( + &*move_.tile.options, &options, + "interactive moved tile options must be \ + base options adjusted for output scale" + ); + + let tile_pos = move_.tile_render_location(); + let rounded_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale); + + // Tile position must be rounded to physical pixels. + assert_abs_diff_eq!(tile_pos.x, rounded_pos.x, epsilon = 1e-5); + assert_abs_diff_eq!(tile_pos.y, rounded_pos.y, epsilon = 1e-5); + } + } + } + let mut seen_workspace_id = HashSet::new(); let mut seen_workspace_name = Vec::<String>::new(); @@ -1760,7 +2012,7 @@ impl<W: LayoutElement> Layout<W> { seen_workspace_name.push(name.clone()); } - workspace.verify_invariants(); + workspace.verify_invariants(move_win_id.as_ref()); } return; @@ -1869,7 +2121,7 @@ impl<W: LayoutElement> Layout<W> { seen_workspace_name.push(name.clone()); } - workspace.verify_invariants(); + workspace.verify_invariants(move_win_id.as_ref()); } } } @@ -1877,6 +2129,10 @@ impl<W: LayoutElement> Layout<W> { pub fn advance_animations(&mut self, current_time: Duration) { let _span = tracy_client::span!("Layout::advance_animations"); + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + move_.tile.advance_animations(current_time); + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -1891,9 +2147,46 @@ impl<W: LayoutElement> Layout<W> { } } + pub fn are_animations_ongoing(&self, output: Option<&Output>) -> bool { + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { + if move_.tile.are_animations_ongoing() { + return true; + } + } + + let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { + return false; + }; + + for mon in monitors { + if output.map_or(false, |output| mon.output != *output) { + continue; + } + + if mon.are_animations_ongoing() { + return true; + } + } + + false + } + pub fn update_render_elements(&mut self, output: Option<&Output>) { let _span = tracy_client::span!("Layout::update_render_elements"); + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + if output.map_or(true, |output| move_.output == *output) { + let pos_within_output = move_.tile_render_location(); + let view_rect = Rectangle::from_loc_and_size( + pos_within_output.upscale(-1.), + output_size(&move_.output), + ); + move_.tile.update(true, view_rect); + } + } + + self.update_insert_hint(output); + let MonitorSet::Normal { monitors, active_monitor_idx, @@ -1906,13 +2199,19 @@ impl<W: LayoutElement> Layout<W> { for (idx, mon) in monitors.iter_mut().enumerate() { if output.map_or(true, |output| mon.output == *output) { - let is_active = self.is_active && idx == *active_monitor_idx; + let is_active = self.is_active + && idx == *active_monitor_idx + && !matches!(self.interactive_move, Some(InteractiveMoveState::Moving(_))); mon.update_render_elements(is_active); } } } pub fn update_shaders(&mut self) { + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + move_.tile.update_shaders(); + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -1929,6 +2228,50 @@ impl<W: LayoutElement> Layout<W> { } } + fn update_insert_hint(&mut self, output: Option<&Output>) { + let _span = tracy_client::span!("Layout::update_insert_hint"); + + let _span = tracy_client::span!("Layout::update_insert_hint::clear"); + for ws in self.workspaces_mut() { + ws.clear_insert_hint(); + } + + if !matches!(self.interactive_move, Some(InteractiveMoveState::Moving(_))) { + return; + } + let Some(InteractiveMoveState::Moving(move_)) = self.interactive_move.take() else { + unreachable!() + }; + if output.map_or(false, |out| &move_.output != out) { + self.interactive_move = Some(InteractiveMoveState::Moving(move_)); + return; + } + + let _span = tracy_client::span!("Layout::update_insert_hint::update"); + + if let Some(mon) = self.monitor_for_output_mut(&move_.output) { + if let Some((ws, offset)) = mon.workspace_under(move_.pointer_pos_within_output) { + let ws_id = ws.id(); + let ws = mon + .workspaces + .iter_mut() + .find(|ws| ws.id() == ws_id) + .unwrap(); + + // TODO: empty workspaces need to reset their view position right away for this to + // look right. + let position = ws.get_insert_position(move_.pointer_pos_within_output - offset); + ws.set_insert_hint(InsertHint { + position, + width: move_.width, + is_full_width: move_.is_full_width, + }); + } + } + + self.interactive_move = Some(InteractiveMoveState::Moving(move_)); + } + pub fn ensure_named_workspace(&mut self, ws_config: &WorkspaceConfig) { if self.find_workspace_by_name(&ws_config.name.0).is_some() { return; @@ -1974,6 +2317,11 @@ impl<W: LayoutElement> Layout<W> { pub fn update_config(&mut self, config: &Config) { let options = Rc::new(Options::from_config(config)); + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + let scale = move_.output.current_scale().fractional_scale(); + move_.tile.update_config(scale, options.clone()); + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -1998,6 +2346,12 @@ impl<W: LayoutElement> Layout<W> { } pub fn toggle_window_height(&mut self, window: Option<&W::Id>) { + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + if window == Some(move_.tile.window().id()) { + return; + } + } + let workspace = if let Some(window) = window { Some( self.workspaces_mut() @@ -2029,6 +2383,12 @@ impl<W: LayoutElement> Layout<W> { } pub fn set_window_height(&mut self, window: Option<&W::Id>, change: SizeChange) { + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + if window == Some(move_.tile.window().id()) { + return; + } + } + let workspace = if let Some(window) = window { Some( self.workspaces_mut() @@ -2046,6 +2406,12 @@ impl<W: LayoutElement> Layout<W> { } pub fn reset_window_height(&mut self, window: Option<&W::Id>) { + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + if window == Some(move_.tile.window().id()) { + return; + } + } + let workspace = if let Some(window) = window { Some( self.workspaces_mut() @@ -2084,6 +2450,12 @@ impl<W: LayoutElement> Layout<W> { output: &Output, target_ws_idx: Option<usize>, ) { + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { + if window == Some(move_.tile.window().id()) { + return; + } + } + if let MonitorSet::Normal { monitors, active_monitor_idx, @@ -2227,6 +2599,12 @@ impl<W: LayoutElement> Layout<W> { } pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) { + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { + if move_.tile.window().id() == window { + return; + } + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -2250,6 +2628,12 @@ impl<W: LayoutElement> Layout<W> { } pub fn toggle_fullscreen(&mut self, window: &W::Id) { + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { + if move_.tile.window().id() == window { + return; + } + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -2402,6 +2786,350 @@ impl<W: LayoutElement> Layout<W> { None } + pub fn interactive_move_begin( + &mut self, + window_id: W::Id, + output: &Output, + start_pos_within_output: Point<f64, Logical>, + ) -> bool { + if self.interactive_move.is_some() { + return false; + } + + let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else { + return false; + }; + + let Some((mon, (ws, ws_offset))) = monitors.iter().find_map(|mon| { + mon.workspaces_with_render_positions() + .find(|(ws, _)| ws.has_window(&window_id)) + .map(|rv| (mon, rv)) + }) else { + return false; + }; + + if mon.output() != output { + return false; + } + + let (tile, tile_offset) = ws + .tiles_with_render_positions() + .find(|(tile, _)| tile.window().id() == &window_id) + .unwrap(); + let window_offset = tile.window_loc(); + + let tile_pos = ws_offset + tile_offset; + + let pointer_offset_within_window = start_pos_within_output - tile_pos - window_offset; + let window_size = tile.window_size(); + let pointer_ratio_within_window = ( + f64::clamp(pointer_offset_within_window.x / window_size.w, 0., 1.), + f64::clamp(pointer_offset_within_window.y / window_size.h, 0., 1.), + ); + + self.interactive_move = Some(InteractiveMoveState::Starting { + window_id, + pointer_delta: Point::from((0., 0.)), + pointer_ratio_within_window, + }); + + true + } + + pub fn interactive_move_update( + &mut self, + window: &W::Id, + delta: Point<f64, Logical>, + output: Output, + pointer_pos_within_output: Point<f64, Logical>, + ) -> bool { + let Some(state) = self.interactive_move.take() else { + return false; + }; + + match state { + InteractiveMoveState::Starting { + window_id, + mut pointer_delta, + pointer_ratio_within_window, + } => { + if window_id != *window { + self.interactive_move = Some(InteractiveMoveState::Starting { + window_id, + pointer_delta, + pointer_ratio_within_window, + }); + return false; + } + + pointer_delta += delta; + + let (cx, cy) = (pointer_delta.x, pointer_delta.y); + let sq_dist = cx * cx + cy * cy; + + let factor = RubberBand { + stiffness: 1.0, + limit: 0.5, + } + .band(sq_dist / INTERACTIVE_MOVE_START_THRESHOLD); + + let tile = self + .workspaces_mut() + .flat_map(|ws| ws.tiles_mut()) + .find(|tile| *tile.window().id() == window_id) + .unwrap(); + tile.interactive_move_offset = pointer_delta.upscale(factor); + + // Put it back to be able to easily return. + self.interactive_move = Some(InteractiveMoveState::Starting { + window_id: window_id.clone(), + pointer_delta, + pointer_ratio_within_window, + }); + + if sq_dist < INTERACTIVE_MOVE_START_THRESHOLD { + return true; + } + + // If the pointer is currently on the window's own output, then we can animate the + // window movement from its current (rubberbanded and possibly moved away) position + // to the pointer. Otherwise, we just teleport it as the layout code is not aware + // of monitor positions. + // + // FIXME: with floating layer, the layout code will know about monitor positions, + // so this will be potentially animatable. + let mut tile_pos = None; + if let MonitorSet::Normal { monitors, .. } = &self.monitor_set { + if let Some((mon, (ws, ws_offset))) = monitors.iter().find_map(|mon| { + mon.workspaces_with_render_positions() + .find(|(ws, _)| ws.has_window(window)) + .map(|rv| (mon, rv)) + }) { + if mon.output() == &output { + let (_, tile_offset) = ws + .tiles_with_render_positions() + .find(|(tile, _)| tile.window().id() == window) + .unwrap(); + + tile_pos = Some(ws_offset + tile_offset); + } + } + } + + let RemovedTile { + mut tile, + width, + is_full_width, + } = self.remove_window(window, Transaction::new()).unwrap(); + + tile.stop_move_animations(); + tile.interactive_move_offset = Point::from((0., 0.)); + tile.window().output_enter(&output); + tile.window().set_preferred_scale_transform( + output.current_scale(), + output.current_transform(), + ); + + let scale = output.current_scale().fractional_scale(); + tile.update_config( + scale, + Rc::new(Options::clone(&self.options).adjusted_for_scale(scale)), + ); + + // Unfullscreen and let the window pick a natural size. + // + // When we have floating, we will want to always send a (0, 0) size here, not just + // to unfullscreen. However, when implementing that, remember to check how GTK + // tiled window size restoration works. It seems to remember *some* last size with + // prefer-no-csd, and occasionally that last size can become the full-width size + // rather than a smaller size, which is annoying. Need to see if niri can use some + // heuristics to make this case behave better. + if tile.window().is_pending_fullscreen() { + tile.window_mut() + .request_size(Size::from((0, 0)), true, None); + } + + let mut data = InteractiveMoveData { + tile, + output, + pointer_pos_within_output, + width, + is_full_width, + pointer_ratio_within_window, + }; + + if |
