diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2025-04-25 10:11:27 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2025-04-25 02:00:18 -0700 |
| commit | 1835b532d92a98808ea2e2568fa9b899c8b24b35 (patch) | |
| tree | b6a35090d411f51e687e833dab5e20e0a8e128e4 /src/layout | |
| parent | e6d82d3ee34a7ac225d07ca3f4f4365cf27292d1 (diff) | |
| download | niri-1835b532d92a98808ea2e2568fa9b899c8b24b35.tar.gz niri-1835b532d92a98808ea2e2568fa9b899c8b24b35.tar.bz2 niri-1835b532d92a98808ea2e2568fa9b899c8b24b35.zip | |
Implement interactive move to a new workspace above/between
Diffstat (limited to 'src/layout')
| -rw-r--r-- | src/layout/mod.rs | 200 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 200 |
2 files changed, 296 insertions, 104 deletions
diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 6adf3ad7..42ae58c5 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -37,7 +37,7 @@ use std::mem; use std::rc::Rc; use std::time::Duration; -use monitor::{InsertHint, InsertPosition, MonitorAddWindowTarget}; +use monitor::{InsertHint, InsertPosition, InsertWorkspace, MonitorAddWindowTarget}; use niri_config::{ CenterFocusedColumn, Config, CornerRadius, FloatOrInt, PresetSize, Struts, Workspace as WorkspaceConfig, WorkspaceReference, @@ -3031,40 +3031,51 @@ impl<W: LayoutElement> Layout<W> { return; } - // No insert hint when targeting floating. - if move_.is_floating { - 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) { let zoom = mon.overview_zoom(); - if let Some((ws, geo)) = mon.workspace_under(move_.pointer_pos_within_output) { - let ws_id = ws.id(); - let ws = mon - .workspaces - .iter_mut() - .find(|ws| ws.id() == ws_id) - .unwrap(); - let pos_within_workspace = - (move_.pointer_pos_within_output - geo.loc).downscale(zoom); - let position = ws.scrolling_insert_position(pos_within_workspace); - - let rules = move_.tile.window().rules(); - let border_width = move_.tile.effective_border_width().unwrap_or(0.); - let corner_radius = rules - .geometry_corner_radius - .map_or(CornerRadius::default(), |radius| { - radius.expanded_by(border_width as f32) - }); + let (insert_ws, geo) = mon.insert_position(move_.pointer_pos_within_output); + match insert_ws { + InsertWorkspace::Existing(ws_id) => { + let ws = mon + .workspaces + .iter_mut() + .find(|ws| ws.id() == ws_id) + .unwrap(); + let pos_within_workspace = + (move_.pointer_pos_within_output - geo.loc).downscale(zoom); + let position = if move_.is_floating { + InsertPosition::Floating + } else { + ws.scrolling_insert_position(pos_within_workspace) + }; - mon.insert_hint = Some(InsertHint { - workspace: ws_id, - position, - corner_radius, - }); + let rules = move_.tile.window().rules(); + let border_width = move_.tile.effective_border_width().unwrap_or(0.); + let corner_radius = rules + .geometry_corner_radius + .map_or(CornerRadius::default(), |radius| { + radius.expanded_by(border_width as f32) + }); + mon.insert_hint = Some(InsertHint { + workspace: insert_ws, + position, + corner_radius, + }); + } + InsertWorkspace::NewAt(_) => { + let position = if move_.is_floating { + InsertPosition::Floating + } else { + InsertPosition::NewColumn(0) + }; + mon.insert_hint = Some(InsertHint { + workspace: insert_ws, + position, + corner_radius: CornerRadius::default(), + }); + } } } @@ -4393,59 +4404,81 @@ impl<W: LayoutElement> Layout<W> { active_monitor_idx, .. } => { - let (mon, ws_idx, position, offset, zoom) = if let Some(mon) = - monitors.iter_mut().find(|mon| mon.output == move_.output) - { - let zoom = mon.overview_zoom(); + let (mon, insert_ws, position, offset, zoom) = + if let Some(mon) = monitors.iter_mut().find(|mon| mon.output == move_.output) { + let zoom = mon.overview_zoom(); + + let (insert_ws, geo) = mon.insert_position(move_.pointer_pos_within_output); + let (position, offset) = match insert_ws { + InsertWorkspace::Existing(ws_id) => { + let ws_idx = mon + .workspaces + .iter_mut() + .position(|ws| ws.id() == ws_id) + .unwrap(); - let (ws, ws_geo) = mon - .workspace_under(move_.pointer_pos_within_output) - // If the pointer is somehow outside the move output and a workspace switch - // is in progress, this won't necessarily do the expected thing. - .unwrap_or_else(|| mon.workspaces_with_render_geo().next().unwrap()); + let position = if move_.is_floating { + InsertPosition::Floating + } else { + let pos_within_workspace = + (move_.pointer_pos_within_output - geo.loc).downscale(zoom); + let ws = &mut mon.workspaces[ws_idx]; + ws.scrolling_insert_position(pos_within_workspace) + }; - let ws_id = ws.id(); - let ws_idx = mon - .workspaces - .iter_mut() - .position(|ws| ws.id() == ws_id) - .unwrap(); + (position, Some(geo.loc)) + } + InsertWorkspace::NewAt(_) => { + let position = if move_.is_floating { + InsertPosition::Floating + } else { + InsertPosition::NewColumn(0) + }; - let position = if move_.is_floating { - InsertPosition::Floating + (position, None) + } + }; + + (mon, insert_ws, position, offset, zoom) } else { - let pos_within_workspace = - (move_.pointer_pos_within_output - ws_geo.loc).downscale(zoom); - let ws = &mut mon.workspaces[ws_idx]; - ws.scrolling_insert_position(pos_within_workspace) - }; + let mon = &mut monitors[*active_monitor_idx]; + let zoom = mon.overview_zoom(); + // No point in trying to use the pointer position on the wrong output. + let (ws, ws_geo) = mon.workspaces_with_render_geo().next().unwrap(); - (mon, ws_idx, position, ws_geo.loc, zoom) - } else { - let mon = &mut monitors[*active_monitor_idx]; - let zoom = mon.overview_zoom(); - // No point in trying to use the pointer position on the wrong output. - let (ws, ws_geo) = mon.workspaces_with_render_geo().next().unwrap(); + let position = if move_.is_floating { + InsertPosition::Floating + } else { + ws.scrolling_insert_position(Point::from((0., 0.))) + }; - let position = if move_.is_floating { - InsertPosition::Floating - } else { - ws.scrolling_insert_position(Point::from((0., 0.))) + let insert_ws = InsertWorkspace::Existing(ws.id()); + (mon, insert_ws, position, Some(ws_geo.loc), zoom) }; - let ws_id = ws.id(); - let ws_idx = mon + let win_id = move_.tile.window().id().clone(); + let window_render_loc = move_.tile_render_location(zoom) + move_.tile.window_loc(); + + let ws_idx = match insert_ws { + InsertWorkspace::Existing(ws_id) => mon .workspaces - .iter_mut() + .iter() .position(|ws| ws.id() == ws_id) - .unwrap(); - - (mon, ws_idx, position, ws_geo.loc, zoom) + .unwrap(), + InsertWorkspace::NewAt(ws_idx) => { + if self.options.empty_workspace_above_first && ws_idx == 0 { + // Reuse the top empty workspace. + 0 + } else if mon.workspaces.len() - 1 <= ws_idx { + // Reuse the bottom empty workspace. + mon.workspaces.len() - 1 + } else { + mon.add_workspace_at(ws_idx); + ws_idx + } + } }; - let win_id = move_.tile.window().id().clone(); - let window_render_loc = move_.tile_render_location(zoom) + move_.tile.window_loc(); - match position { InsertPosition::NewColumn(column_idx) => { let ws_id = mon.workspaces[ws_idx].id(); @@ -4474,10 +4507,27 @@ impl<W: LayoutElement> Layout<W> { let tile_render_loc = move_.tile_render_location(zoom); let mut tile = move_.tile; - - let pos = (tile_render_loc - offset).downscale(zoom); - let pos = mon.workspaces[ws_idx].floating_logical_to_size_frac(pos); - tile.floating_pos = Some(pos); + tile.floating_pos = None; + + match insert_ws { + InsertWorkspace::Existing(_) => { + if let Some(offset) = offset { + let pos = (tile_render_loc - offset).downscale(zoom); + let pos = + mon.workspaces[ws_idx].floating_logical_to_size_frac(pos); + tile.floating_pos = Some(pos); + } else { + error!( + "offset unset for inserting a floating tile \ + to existing workspace" + ); + } + } + InsertWorkspace::NewAt(_) => { + // When putting a floating tile on a new workspace, we don't really + // have a good pre-existing position. + } + } // Set the floating size so it takes into account any window resizing that // took place during the move. diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index e2c37954..ec37d749 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -127,16 +127,22 @@ pub(super) enum InsertPosition { Floating, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum InsertWorkspace { + Existing(WorkspaceId), + NewAt(usize), +} + #[derive(Debug)] pub(super) struct InsertHint { - pub workspace: WorkspaceId, + pub workspace: InsertWorkspace, pub position: InsertPosition, pub corner_radius: CornerRadius, } #[derive(Debug, Clone, Copy)] struct InsertHintRenderLoc { - workspace: WorkspaceId, + workspace: InsertWorkspace, location: Point<f64, Logical>, } @@ -167,6 +173,7 @@ niri_render_elements! { MonitorInnerRenderElement<R> => { Workspace = CropRenderElement<WorkspaceRenderElement<R>>, InsertHint = CropRenderElement<InsertHintRenderElement>, + UncroppedInsertHint = InsertHintRenderElement, Shadow = ShadowRenderElement, } } @@ -231,6 +238,15 @@ impl WorkspaceSwitchGesture { } } +impl InsertWorkspace { + fn existing_id(self) -> Option<WorkspaceId> { + match self { + InsertWorkspace::Existing(id) => Some(id), + InsertWorkspace::NewAt(_) => None, + } + } +} + impl OverviewProgress { pub fn value(&self) -> f64 { match self { @@ -885,7 +901,10 @@ impl<W: LayoutElement> Monitor<W> { pub fn update_render_elements(&mut self, is_active: bool) { let mut insert_hint_ws_geo = None; - let insert_hint_ws_id = self.insert_hint.as_ref().map(|hint| hint.workspace); + let insert_hint_ws_id = self + .insert_hint + .as_ref() + .and_then(|hint| hint.workspace.existing_id()); for (ws, geo) in self.workspaces_with_render_geo_mut() { ws.update_render_elements(is_active); @@ -897,38 +916,75 @@ impl<W: LayoutElement> Monitor<W> { self.insert_hint_render_loc = None; if let Some(hint) = &self.insert_hint { - if let Some(ws) = self.workspaces.iter().find(|ws| ws.id() == hint.workspace) { - if let Some(mut area) = ws.insert_hint_area(hint.position) { - let scale = ws.scale().fractional_scale(); - let view_size = ws.view_size(); - - // Make sure the hint is at least partially visible. - if matches!(hint.position, InsertPosition::NewColumn(_)) { - let zoom = self.overview_zoom(); - let geo = insert_hint_ws_geo.unwrap(); - let geo = geo.downscale(zoom); - - area.loc.x = area.loc.x.max(-geo.loc.x - area.size.w / 2.); - area.loc.x = area.loc.x.min(geo.loc.x + geo.size.w - area.size.w / 2.); + match hint.workspace { + InsertWorkspace::Existing(ws_id) => { + if let Some(ws) = self.workspaces.iter().find(|ws| ws.id() == ws_id) { + if let Some(mut area) = ws.insert_hint_area(hint.position) { + let scale = ws.scale().fractional_scale(); + let view_size = ws.view_size(); + + // Make sure the hint is at least partially visible. + if matches!(hint.position, InsertPosition::NewColumn(_)) { + let zoom = self.overview_zoom(); + let geo = insert_hint_ws_geo.unwrap(); + let geo = geo.downscale(zoom); + + area.loc.x = area.loc.x.max(-geo.loc.x - area.size.w / 2.); + area.loc.x = + area.loc.x.min(geo.loc.x + geo.size.w - area.size.w / 2.); + } + + // Round to physical pixels. + area = area.to_physical_precise_round(scale).to_logical(scale); + + let view_rect = Rectangle::new(area.loc.upscale(-1.), view_size); + self.insert_hint_element.update_render_elements( + area.size, + view_rect, + hint.corner_radius, + scale, + ); + self.insert_hint_render_loc = Some(InsertHintRenderLoc { + workspace: hint.workspace, + location: area.loc, + }); + } + } else { + error!("insert hint workspace missing from monitor"); } + } + InsertWorkspace::NewAt(ws_idx) => { + let scale = self.scale.fractional_scale(); + let zoom = self.overview_zoom(); + let gap = self.workspace_gap(zoom); + + let hint_gap = round_logical_in_physical(scale, gap * 0.1); + let hint_height = gap - hint_gap * 2.; - // Round to physical pixels. - area = area.to_physical_precise_round(scale).to_logical(scale); + let next_ws_geo = self.workspaces_render_geo().nth(ws_idx).unwrap(); + let hint_loc_diff = Point::from((0., hint_height + hint_gap)); + let hint_loc = next_ws_geo.loc - hint_loc_diff; + let hint_size = Size::from((next_ws_geo.size.w, hint_height)); + + // FIXME: sometimes the hint ends up 1 px wider than necessary and/or 1 px + // narrower than necessary. The values here seem correct. Might have to do with + // how zooming out currently doesn't round to output scale properly. + + // Compute view rect as if we're above the next workspace (rather than below + // the previous one). + let view_rect = Rectangle::new(hint_loc_diff, next_ws_geo.size); - let view_rect = Rectangle::new(area.loc.upscale(-1.), view_size); self.insert_hint_element.update_render_elements( - area.size, + hint_size, view_rect, - hint.corner_radius, + CornerRadius::default(), scale, ); self.insert_hint_render_loc = Some(InsertHintRenderLoc { workspace: hint.workspace, - location: area.loc, + location: hint_loc, }); } - } else { - error!("insert hint workspace missing from monitor"); } } } @@ -1239,6 +1295,17 @@ impl<W: LayoutElement> Monitor<W> { .filter(move |(_ws, geo)| geo.intersection(output_geo).is_some()) } + pub fn workspaces_with_render_geo_idx( + &self, + ) -> impl Iterator<Item = ((usize, &Workspace<W>), Rectangle<f64, Logical>)> { + let output_geo = Rectangle::from_size(self.view_size); + + let geo = self.workspaces_render_geo(); + zip(self.workspaces.iter().enumerate(), geo) + // Cull out workspaces outside the output. + .filter(move |(_ws, geo)| geo.intersection(output_geo).is_some()) + } + pub fn workspaces_with_render_geo_mut( &mut self, ) -> impl Iterator<Item = (&mut Workspace<W>, Rectangle<f64, Logical>)> { @@ -1298,6 +1365,55 @@ impl<W: LayoutElement> Monitor<W> { ws.resize_edges_under(pos_within_output - geo.loc) } + pub(super) fn insert_position( + &self, + pos_within_output: Point<f64, Logical>, + ) -> (InsertWorkspace, Rectangle<f64, Logical>) { + let mut iter = self.workspaces_with_render_geo_idx(); + + let dummy = Rectangle::default(); + + // Monitors always have at least one workspace. + let ((idx, ws), geo) = iter.next().unwrap(); + + // Check if above first. + if pos_within_output.y < geo.loc.y { + return (InsertWorkspace::NewAt(idx), dummy); + } + + let contains = move |geo: Rectangle<f64, Logical>| { + geo.loc.y <= pos_within_output.y && pos_within_output.y < geo.loc.y + geo.size.h + }; + + // Check first. + if contains(geo) { + return (InsertWorkspace::Existing(ws.id()), geo); + } + + let mut last_geo = geo; + let mut last_idx = idx; + for ((idx, ws), geo) in iter { + // Check gap above. + let gap_loc = Point::from((last_geo.loc.x, last_geo.loc.y + last_geo.size.h)); + let gap_size = Size::from((geo.size.w, geo.loc.y - gap_loc.y)); + let gap_geo = Rectangle::new(gap_loc, gap_size); + if contains(gap_geo) { + return (InsertWorkspace::NewAt(idx), dummy); + } + + // Check workspace itself. + if contains(geo) { + return (InsertWorkspace::Existing(ws.id()), geo); + } + + last_geo = geo; + last_idx = idx; + } + + // Anything below. + (InsertWorkspace::NewAt(last_idx + 1), dummy) + } + pub fn render_above_top_layer(&self) -> bool { // Render above the top layer only if the view is stationary. if self.workspace_switch.is_some() || self.overview_progress.is_some() { @@ -1308,6 +1424,30 @@ impl<W: LayoutElement> Monitor<W> { ws.render_above_top_layer() } + pub fn render_insert_hint_between_workspaces<R: NiriRenderer>( + &self, + renderer: &mut R, + ) -> impl Iterator<Item = MonitorRenderElement<R>> { + let mut rv = None; + + if !self.options.insert_hint.off { + if let Some(render_loc) = self.insert_hint_render_loc { + if let InsertWorkspace::NewAt(_) = render_loc.workspace { + let iter = self + .insert_hint_element + .render(renderer, render_loc.location) + .map(MonitorInnerRenderElement::UncroppedInsertHint); + rv = Some(iter); + } + } + } + + rv.into_iter().flatten().map(|elem| { + let elem = RescaleRenderElement::from_element(elem, Point::default(), 1.); + RelocateRenderElement::from_element(elem, Point::default(), Relocate::Relative) + }) + } + pub fn render_elements<'a, R: NiriRenderer>( &'a self, renderer: &'a mut R, @@ -1354,11 +1494,13 @@ impl<W: LayoutElement> Monitor<W> { let mut insert_hint = None; if !self.options.insert_hint.off { if let Some(render_loc) = self.insert_hint_render_loc { - insert_hint = Some(( - render_loc.workspace, - self.insert_hint_element - .render(renderer, render_loc.location), - )); + if let InsertWorkspace::Existing(workspace_id) = render_loc.workspace { + insert_hint = Some(( + workspace_id, + self.insert_hint_element + .render(renderer, render_loc.location), + )); + } } } |
