aboutsummaryrefslogtreecommitdiff
path: root/src/layout
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-04-25 09:36:50 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-04-25 02:00:18 -0700
commitaf1fca35bb15b8010cd3a12bbafe71b55d9ecf57 (patch)
tree6283896fd931b9e5244a435cee9a0c227a850c23 /src/layout
parent9571d149b2cecd3df8ba3f90f0af296e9f69af6e (diff)
downloadniri-af1fca35bb15b8010cd3a12bbafe71b55d9ecf57.tar.gz
niri-af1fca35bb15b8010cd3a12bbafe71b55d9ecf57.tar.bz2
niri-af1fca35bb15b8010cd3a12bbafe71b55d9ecf57.zip
Implement an Overview
Diffstat (limited to 'src/layout')
-rw-r--r--src/layout/mod.rs438
-rw-r--r--src/layout/monitor.rs236
-rw-r--r--src/layout/tests.rs20
-rw-r--r--src/layout/workspace.rs53
4 files changed, 681 insertions, 66 deletions
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index cdb96113..019df993 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -45,6 +45,7 @@ use niri_config::{
use niri_ipc::{ColumnDisplay, PositionChange, SizeChange};
use scrolling::{Column, ColumnWidth};
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
+use smithay::backend::renderer::element::utils::RescaleRenderElement;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::output::{self, Output};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@@ -55,7 +56,8 @@ use workspace::{WorkspaceAddWindowTarget, WorkspaceId};
pub use self::monitor::MonitorRenderElement;
use self::monitor::{Monitor, WorkspaceSwitch};
use self::workspace::{OutputId, Workspace};
-use crate::animation::Clock;
+use crate::animation::{Animation, Clock};
+use crate::input::swipe_tracker::SwipeTracker;
use crate::layout::scrolling::ScrollDirection;
use crate::niri_render_elements;
use crate::render_helpers::offscreen::OffscreenData;
@@ -96,6 +98,14 @@ const INTERACTIVE_MOVE_START_THRESHOLD: f64 = 256. * 256.;
/// Opacity of interactively moved tiles targeting the scrolling layout.
const INTERACTIVE_MOVE_ALPHA: f64 = 0.75;
+/// Amount of touchpad movement to toggle the overview.
+const OVERVIEW_GESTURE_MOVEMENT: f64 = 300.;
+
+const OVERVIEW_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
+ stiffness: 0.5,
+ limit: 0.05,
+};
+
/// Size-relative units.
pub struct SizeFrac;
@@ -293,6 +303,13 @@ pub struct Layout<W: LayoutElement> {
clock: Clock,
/// Time that we last updated render elements for.
update_render_elements_time: Duration,
+ /// Whether the overview is open.
+ ///
+ /// This is a boolean flag that controls things like where input goes to. The actual animation
+ /// is controlled by overview_progress.
+ overview_open: bool,
+ /// The overview zoom progress.
+ overview_progress: Option<OverviewProgress>,
/// Configurable properties of the layout.
options: Rc<Options>,
}
@@ -338,6 +355,7 @@ pub struct Options {
pub preset_window_heights: Vec<PresetSize>,
pub animations: niri_config::Animations,
pub gestures: niri_config::Gestures,
+ pub overview: niri_config::Overview,
// Debug flags.
pub disable_resize_throttling: bool,
pub disable_transactions: bool,
@@ -365,6 +383,7 @@ impl Default for Options {
default_column_width: None,
animations: Default::default(),
gestures: Default::default(),
+ overview: Default::default(),
disable_resize_throttling: false,
disable_transactions: false,
preset_window_heights: vec![
@@ -493,6 +512,21 @@ pub enum HitType {
},
}
+#[derive(Debug)]
+enum OverviewProgress {
+ Animation(Animation),
+ Gesture(OverviewGesture),
+}
+
+#[derive(Debug)]
+struct OverviewGesture {
+ tracker: SwipeTracker,
+ /// Start point.
+ start: f64,
+ /// Current progress.
+ value: f64,
+}
+
impl<W: LayoutElement> InteractiveMoveState<W> {
fn moving(&self) -> Option<&InteractiveMoveData<W>> {
match self {
@@ -510,16 +544,16 @@ impl<W: LayoutElement> InteractiveMoveState<W> {
}
impl<W: LayoutElement> InteractiveMoveData<W> {
- fn tile_render_location(&self) -> Point<f64, Logical> {
+ fn tile_render_location(&self, zoom: f64) -> 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();
+ let pos = self.pointer_pos_within_output
+ - (pointer_offset_within_window + self.tile.window_loc() - self.tile.render_offset())
+ .upscale(zoom);
// Round to physical pixels.
pos.to_physical_precise_round(scale).to_logical(scale)
}
@@ -553,6 +587,15 @@ impl HitType {
tile.hit(pos_within_tile)
.map(|hit| (tile.window(), hit.offset_win_pos(tile_pos)))
}
+
+ pub fn to_activate(self) -> Self {
+ match self {
+ HitType::Input { .. } => HitType::Activate {
+ is_tab_indicator: false,
+ },
+ HitType::Activate { .. } => self,
+ }
+ }
}
impl Options {
@@ -594,6 +637,7 @@ impl Options {
default_column_width,
animations: config.animations.clone(),
gestures: config.gestures,
+ overview: config.overview,
disable_resize_throttling: config.debug.disable_resize_throttling,
disable_transactions: config.debug.disable_transactions,
preset_window_heights,
@@ -611,6 +655,19 @@ impl Options {
}
}
+impl OverviewProgress {
+ fn value(&self) -> f64 {
+ match self {
+ OverviewProgress::Animation(anim) => anim.value(),
+ OverviewProgress::Gesture(gesture) => gesture.value,
+ }
+ }
+
+ fn is_animation(&self) -> bool {
+ matches!(self, OverviewProgress::Animation(_))
+ }
+}
+
impl<W: LayoutElement> Layout<W> {
pub fn new(clock: Clock, config: &Config) -> Self {
Self::with_options_and_workspaces(clock, config, Options::from_config(config))
@@ -625,6 +682,8 @@ impl<W: LayoutElement> Layout<W> {
dnd: None,
clock,
update_render_elements_time: Duration::ZERO,
+ overview_open: false,
+ overview_progress: None,
options: Rc::new(options),
}
}
@@ -648,6 +707,8 @@ impl<W: LayoutElement> Layout<W> {
dnd: None,
clock,
update_render_elements_time: Duration::ZERO,
+ overview_open: false,
+ overview_progress: None,
options: opts,
}
}
@@ -751,6 +812,8 @@ impl<W: LayoutElement> Layout<W> {
let mut monitor =
Monitor::new(output, workspaces, self.clock.clone(), self.options.clone());
monitor.active_workspace_idx = active_workspace_idx;
+ monitor.overview_open = self.overview_open;
+ monitor.set_overview_progress(self.overview_progress.as_ref());
monitors.push(monitor);
MonitorSet::Normal {
@@ -789,6 +852,8 @@ impl<W: LayoutElement> Layout<W> {
let mut monitor =
Monitor::new(output, workspaces, self.clock.clone(), self.options.clone());
monitor.active_workspace_idx = active_workspace_idx;
+ monitor.overview_open = self.overview_open;
+ monitor.set_overview_progress(self.overview_progress.as_ref());
MonitorSet::Normal {
monitors: vec![monitor],
@@ -1418,7 +1483,7 @@ impl<W: LayoutElement> Layout<W> {
let mut target = Rectangle::from_size(Size::from((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_render_location(1.).y;
target.loc.y -= move_.tile.window_loc().y;
return target;
}
@@ -2279,8 +2344,19 @@ impl<W: LayoutElement> Layout<W> {
) -> Option<(&W, HitType)> {
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.output == *output {
- let tile_pos = move_.tile_render_location();
- HitType::hit_tile(&move_.tile, tile_pos, pos_within_output)
+ if self.overview_progress.is_some() {
+ let zoom = self.overview_zoom();
+ let tile_pos = move_.tile_render_location(zoom);
+ let pos_within_tile = (pos_within_output - tile_pos).downscale(zoom);
+ // During the overview animation, we cannot do input hits because we cannot
+ // really represent scaled windows properly.
+ let (win, hit) =
+ HitType::hit_tile(&move_.tile, Point::from((0., 0.)), pos_within_tile)?;
+ Some((win, hit.to_activate()))
+ } else {
+ let tile_pos = move_.tile_render_location(1.);
+ HitType::hit_tile(&move_.tile, tile_pos, pos_within_output)
+ }
} else {
None
}
@@ -2316,6 +2392,36 @@ impl<W: LayoutElement> Layout<W> {
mon.resize_edges_under(pos_within_output)
}
+ pub fn workspace_under(
+ &self,
+ extended_bounds: bool,
+ output: &Output,
+ pos_within_output: Point<f64, Logical>,
+ ) -> Option<&Workspace<W>> {
+ if self
+ .interactive_moved_window_under(output, pos_within_output)
+ .is_some()
+ {
+ return None;
+ }
+
+ let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
+ return None;
+ };
+
+ let mon = monitors.iter().find(|mon| &mon.output == output)?;
+ if extended_bounds {
+ mon.workspace_under(pos_within_output).map(|(ws, _)| ws)
+ } else {
+ mon.workspace_under_narrow(pos_within_output)
+ }
+ }
+
+ pub fn overview_zoom(&self) -> f64 {
+ let progress = self.overview_progress.as_ref().map(|p| p.value());
+ compute_overview_zoom(&self.options, progress)
+ }
+
#[cfg(test)]
fn verify_invariants(&self) {
use std::collections::HashSet;
@@ -2324,6 +2430,8 @@ impl<W: LayoutElement> Layout<W> {
use crate::layout::monitor::WorkspaceSwitch;
+ let zoom = self.overview_zoom();
+
let mut move_win_id = None;
if let Some(state) = &self.interactive_move {
match state {
@@ -2352,7 +2460,7 @@ impl<W: LayoutElement> Layout<W> {
base options adjusted for output scale"
);
- let tile_pos = move_.tile_render_location();
+ let tile_pos = move_.tile_render_location(zoom);
let rounded_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale);
// Tile position must be rounded to physical pixels.
@@ -2460,6 +2568,12 @@ impl<W: LayoutElement> Layout<W> {
"monitor options must be synchronized with layout"
);
+ assert_eq!(self.overview_open, monitor.overview_open);
+ assert_eq!(
+ self.overview_progress.as_ref().map(|p| p.value()),
+ monitor.overview_progress_value()
+ );
+
if let Some(WorkspaceSwitch::Animation(anim)) = &monitor.workspace_switch {
let before_idx = anim.from() as usize;
let after_idx = anim.to() as usize;
@@ -2650,6 +2764,8 @@ impl<W: LayoutElement> Layout<W> {
// Scroll the view if needed.
if let Some((output, pos_within_output)) = dnd_scroll {
if let Some(mon) = self.monitor_for_output_mut(&output) {
+ let zoom = mon.overview_zoom();
+
if let Some((ws, geo)) = mon.workspace_under(pos_within_output) {
let ws_id = ws.id();
let ws = mon
@@ -2657,7 +2773,18 @@ impl<W: LayoutElement> Layout<W> {
.iter_mut()
.find(|ws| ws.id() == ws_id)
.unwrap();
- ws.dnd_scroll_gesture_scroll(pos_within_output - geo.loc);
+ // As far as the DnD scroll gesture is concerned, the workspace spans across
+ // the whole monitor horizontally.
+ let ws_pos = Point::from((0., geo.loc.y));
+ ws.dnd_scroll_gesture_scroll(pos_within_output - ws_pos, 1. / zoom);
+ }
+ }
+ }
+
+ if !self.overview_open {
+ if let Some(OverviewProgress::Animation(anim)) = &mut self.overview_progress {
+ if anim.is_done() {
+ self.overview_progress = None;
}
}
}
@@ -2665,6 +2792,7 @@ impl<W: LayoutElement> Layout<W> {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
+ mon.set_overview_progress(self.overview_progress.as_ref());
mon.advance_animations();
}
}
@@ -2697,6 +2825,14 @@ impl<W: LayoutElement> Layout<W> {
}
}
+ if self
+ .overview_progress
+ .as_ref()
+ .is_some_and(|p| p.is_animation())
+ {
+ return true;
+ }
+
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return false;
};
@@ -2719,9 +2855,10 @@ impl<W: LayoutElement> Layout<W> {
self.update_render_elements_time = self.clock.now();
+ let zoom = self.overview_zoom();
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 pos_within_output = move_.tile_render_location(zoom);
let view_rect =
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output));
move_.tile.update_render_elements(true, view_rect);
@@ -2745,6 +2882,7 @@ impl<W: LayoutElement> Layout<W> {
let is_active = self.is_active
&& idx == *active_monitor_idx
&& !matches!(self.interactive_move, Some(InteractiveMoveState::Moving(_)));
+ mon.set_overview_progress(self.overview_progress.as_ref());
mon.update_render_elements(is_active);
}
}
@@ -2798,6 +2936,7 @@ impl<W: LayoutElement> Layout<W> {
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
@@ -2805,8 +2944,8 @@ impl<W: LayoutElement> Layout<W> {
.iter_mut()
.find(|ws| ws.id() == ws_id)
.unwrap();
-
- let pos_within_workspace = move_.pointer_pos_within_output - geo.loc;
+ 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();
@@ -3645,6 +3784,9 @@ impl<W: LayoutElement> Layout<W> {
timestamp: Duration,
is_touchpad: bool,
) -> Option<Option<Output>> {
+ let zoom = self.overview_zoom();
+ let delta_x = delta_x / zoom;
+
let monitors = match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => monitors,
MonitorSet::NoOutputs { .. } => return None,
@@ -3684,6 +3826,77 @@ impl<W: LayoutElement> Layout<W> {
None
}
+ pub fn overview_gesture_begin(&mut self) {
+ self.overview_open = true;
+
+ let value = self.overview_progress.take().map_or(0., |p| p.value());
+ let gesture = OverviewGesture {
+ tracker: SwipeTracker::new(),
+ start: value,
+ value,
+ };
+ self.overview_progress = Some(OverviewProgress::Gesture(gesture));
+
+ self.set_monitors_overview_state();
+ }
+
+ pub fn overview_gesture_update(&mut self, delta_y: f64, timestamp: Duration) -> Option<bool> {
+ let Some(OverviewProgress::Gesture(gesture)) = &mut self.overview_progress else {
+ return None;
+ };
+
+ gesture.tracker.push(delta_y, timestamp);
+
+ let total_height = OVERVIEW_GESTURE_MOVEMENT;
+ let pos = gesture.tracker.pos() / total_height;
+ let new_value = gesture.start + pos;
+ let new_value = OVERVIEW_GESTURE_RUBBER_BAND.clamp(0., 1., new_value);
+
+ if gesture.value == new_value {
+ return Some(false);
+ }
+
+ gesture.value = new_value;
+ self.set_monitors_overview_state();
+
+ Some(true)
+ }
+
+ pub fn overview_gesture_end(&mut self) -> bool {
+ let Some(OverviewProgress::Gesture(gesture)) = &mut self.overview_progress else {
+ return false;
+ };
+
+ // Take into account any idle time between the last event and now.
+ let now = self.clock.now_unadjusted();
+ gesture.tracker.push(0., now);
+
+ let total_height = OVERVIEW_GESTURE_MOVEMENT;
+
+ let mut velocity = gesture.tracker.velocity() / total_height;
+ let current_pos = gesture.tracker.pos() / total_height;
+ let pos = gesture.tracker.projected_end_pos() / total_height;
+
+ let new_value = gesture.start + pos;
+ let new_value = new_value.clamp(0., 1.).round();
+
+ velocity *=
+ OVERVIEW_GESTURE_RUBBER_BAND.clamp_derivative(0., 1., gesture.start + current_pos);
+
+ self.overview_open = new_value == 1.;
+ self.overview_progress = Some(OverviewProgress::Animation(Animation::new(
+ self.clock.clone(),
+ gesture.value,
+ new_value,
+ velocity,
+ self.options.animations.overview_open_close.0,
+ )));
+
+ self.set_monitors_overview_state();
+
+ true
+ }
+
pub fn interactive_move_begin(
&mut self,
window_id: W::Id,
@@ -3710,6 +3923,8 @@ impl<W: LayoutElement> Layout<W> {
return false;
}
+ let zoom = mon.overview_zoom();
+
let is_floating = ws.is_floating(&window_id);
let (tile, tile_offset, _visible) = ws
.tiles_with_render_positions()
@@ -3717,10 +3932,11 @@ impl<W: LayoutElement> Layout<W> {
.unwrap();
let window_offset = tile.window_loc();
- let tile_pos = ws_geo.loc + tile_offset;
+ let tile_pos = ws_geo.loc + tile_offset.upscale(zoom);
- let pointer_offset_within_window = start_pos_within_output - tile_pos - window_offset;
- let window_size = tile.window_size();
+ let pointer_offset_within_window =
+ start_pos_within_output - tile_pos - window_offset.upscale(zoom);
+ let window_size = tile.window_size().upscale(zoom);
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.),
@@ -3768,6 +3984,9 @@ impl<W: LayoutElement> Layout<W> {
return false;
}
+ let zoom = self.overview_zoom();
+ let delta = delta.downscale(zoom);
+
pointer_delta += delta;
let (cx, cy) = (pointer_delta.x, pointer_delta.y);
@@ -3824,7 +4043,8 @@ impl<W: LayoutElement> Layout<W> {
.find(|(tile, _, _)| tile.window().id() == window)
.unwrap();
- tile_pos = Some(ws_geo.loc + tile_offset);
+ let zoom = mon.overview_zoom();
+ tile_pos = Some((ws_geo.loc + tile_offset.upscale(zoom), zoom));
}
}
}
@@ -3903,9 +4123,10 @@ impl<W: LayoutElement> Layout<W> {
pointer_ratio_within_window,
};
- if let Some(tile_pos) = tile_pos {
- let new_tile_pos = data.tile_render_location();
- data.tile.animate_move_from(tile_pos - new_tile_pos);
+ if let Some((tile_pos, zoom)) = tile_pos {
+ let new_tile_pos = data.tile_render_location(zoom);
+ data.tile
+ .animate_move_from((tile_pos - new_tile_pos).downscale(zoom));
}
self.interactive_move = Some(InteractiveMoveState::Moving(data));
@@ -3960,12 +4181,16 @@ impl<W: LayoutElement> Layout<W> {
unreachable!()
};
+ let mut ws_id = None;
for ws in self.workspaces_mut() {
+ let id = ws.id();
if let Some(tile) = ws.tiles_mut().find(|tile| *tile.window().id() == window_id)
{
let offset = tile.interactive_move_offset;
tile.interactive_move_offset = Point::from((0., 0.));
tile.animate_move_from(offset);
+
+ ws_id = Some(id);
}
// Unlock the view on the workspaces, but if the moved window was active,
@@ -3980,6 +4205,32 @@ impl<W: LayoutElement> Layout<W> {
}
}
+ // In the overview, we want to click on a window to focus it, and also to
+ // click-and-drag to move the window. The way we handle this is by always starting
+ // the interactive move (to get frozen view), then, when in the overview, *not*
+ // calling interactive_move_update() until the cursor moves far enough. This means
+ // that if we "just click" then we end up in this branch with state == Starting.
+ // Close the overview in this case.
+ if self.overview_open {
+ let ws_id = ws_id.unwrap();
+ if let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set {
+ for mon in monitors {
+ if let Some(ws_idx) =
+ mon.workspaces.iter().position(|ws| ws.id() == ws_id)
+ {
+ mon.activate_workspace_with_anim_config(
+ ws_idx,
+ Some(self.options.animations.overview_open_close.0),
+ );
+ break;
+ }
+ }
+ }
+
+ self.activate_window(&window_id);
+ self.close_overview();
+ }
+
return;
}
InteractiveMoveState::Moving(move_) => move_,
@@ -4007,20 +4258,28 @@ impl<W: LayoutElement> Layout<W> {
);
}
+ // Dragging in the overview shouldn't switch the workspace and so on.
+ let activate = if self.overview_open {
+ ActivateWindow::No
+ } else {
+ ActivateWindow::Yes
+ };
+
match &mut self.monitor_set {
MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} => {
- let (mon, ws_idx, position, offset) = if let Some(mon) =
+ 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 (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, but also
- // that is not really supposed to happen so eh?
+ // is in progress, this won't necessarily do the expected thing.
.unwrap_or_else(|| mon.workspaces_with_render_geo().next().unwrap());
let ws_id = ws.id();
@@ -4033,14 +4292,16 @@ impl<W: LayoutElement> Layout<W> {
let position = if move_.is_floating {
InsertPosition::Floating
} else {
- let pos_within_workspace = move_.pointer_pos_within_output - ws_geo.loc;
+ 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)
};
- (mon, ws_idx, position, ws_geo.loc)
+ (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();
@@ -4056,11 +4317,12 @@ impl<W: LayoutElement> Layout<W> {
.iter_mut()
.position(|ws| ws.id() == ws_id)
.unwrap();
- (mon, ws_idx, position, ws_geo.loc)
+
+ (mon, ws_idx, position, ws_geo.loc, zoom)
};
let win_id = move_.tile.window().id().clone();
- let window_render_loc = move_.tile_render_location() + move_.tile.window_loc();
+ let window_render_loc = move_.tile_render_location(zoom) + move_.tile.window_loc();
match position {
InsertPosition::NewColumn(column_idx) => {
@@ -4071,7 +4333,7 @@ impl<W: LayoutElement> Layout<W> {
id: ws_id,
column_idx: Some(column_idx),
},
- ActivateWindow::Yes,
+ activate,
move_.width,
move_.is_full_width,
false,
@@ -4083,13 +4345,15 @@ impl<W: LayoutElement> Layout<W> {
column_idx,
Some(tile_idx),
move_.tile,
- true,
+ activate == ActivateWindow::Yes,
);
}
InsertPosition::Floating => {
- let pos = move_.tile_render_location() - offset;
+ 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);
@@ -4106,7 +4370,7 @@ impl<W: LayoutElement> Layout<W> {
id: ws_id,
column_idx: None,
},
- ActivateWindow::Yes,
+ activate,
move_.width,
move_.is_full_width,
true,
@@ -4115,15 +4379,18 @@ impl<W: LayoutElement> Layout<W> {
}
// needed because empty_workspace_above_first could have modified the idx
- let ws_idx = mon.active_workspace_idx();
- let ws = &mut mon.workspaces[ws_idx];
- let (tile, tile_render_loc) = ws
- .tiles_with_render_positions_mut(false)
- .find(|(tile, _)| tile.window().id() == &win_id)
+ let (tile, tile_render_loc, ws_geo) = mon
+ .workspaces_with_render_geo_mut()
+ .find_map(|(ws, geo)| {
+ ws.tiles_with_render_positions_mut(false)
+ .find(|(tile, _)| tile.window().id() == &win_id)
+ .map(|(tile, tile_render_loc)| (tile, tile_render_loc, geo))
+ })
.unwrap();
- let new_window_render_loc = offset + tile_render_loc + tile.window_loc();
+ let new_window_render_loc =
+ ws_geo.loc + (tile_render_loc + tile.window_loc()).upscale(zoom);
- tile.animate_move_from(window_render_loc - new_window_render_loc);
+ tile.animate_move_from((window_render_loc - new_window_render_loc).downscale(zoom));
}
MonitorSet::NoOutputs { workspaces, .. } => {
if workspaces.is_empty() {
@@ -4138,7 +4405,7 @@ impl<W: LayoutElement> Layout<W> {
ws.add_tile(
move_.tile,
WorkspaceAddWindowTarget::Auto,
- ActivateWindow::Yes,
+ activate,
move_.width,
move_.is_full_width,
move_.is_floating,
@@ -4376,6 +4643,60 @@ impl<W: LayoutElement> Layout<W> {
self.unname_workspace_by_id(id);
}
+ pub fn set_monitors_overview_state(&mut self) {
+ let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else {
+ return;
+ };
+
+ for mon in monitors {
+ mon.overview_open = self.overview_open;
+ mon.set_overview_progress(self.overview_progress.as_ref());
+ }
+ }
+
+ pub fn toggle_overview(&mut self) {
+ self.overview_open = !self.overview_open;
+
+ let from = self.overview_progress.take().map_or(0., |p| p.value());
+ let to = if self.overview_open { 1. } else { 0. };
+
+ self.overview_progress = Some(OverviewProgress::Animation(Animation::new(
+ self.clock.clone(),
+ from,
+ to,
+ 0.,
+ self.options.animations.overview_open_close.0,
+ )));
+
+ self.set_monitors_overview_state();
+ }
+
+ pub fn open_overview(&mut self) -> bool {
+ if self.overview_open {
+ return false;
+ }
+
+ self.toggle_overview();
+ true
+ }
+
+ pub fn close_overview(&mut self) -> bool {
+ if !self.overview_open {
+ return false;
+ }
+
+ self.toggle_overview();
+ true
+ }
+
+ pub fn toggle_overview_to_workspace(&mut self, ws_idx: usize) {
+ let config = self.options.animations.overview_open_close.0;
+ if let Some(mon) = self.active_monitor() {
+ mon.activate_workspace_with_anim_config(ws_idx, Some(config));
+ }
+ self.toggle_overview();
+ }
+
pub fn start_open_animation_for_window(&mut self, window: &W::Id) {
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().id() == window {
@@ -4460,12 +4781,14 @@ impl<W: LayoutElement> Layout<W> {
) {
let _span = tracy_client::span!("Layout::start_close_animation_for_window");
+ let zoom = self.overview_zoom();
+
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if move_.tile.window().id() == window {
let Some(snapshot) = move_.tile.take_unmap_snapshot() else {
return;
};
- let tile_pos = move_.tile_render_location();
+ let tile_pos = move_.tile_render_location(zoom);
let tile_size = move_.tile.tile_size();
let output = move_.output.clone();
@@ -4516,7 +4839,7 @@ impl<W: LayoutElement> Layout<W> {
renderer: &mut R,
output: &Output,
target: RenderTarget,
- ) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
+ ) -> impl Iterator<Item = RescaleRenderElement<TileRenderElement<R>>> + 'a {
if self.update_render_elements_time != self.clock.now() {
error!("clock moved between updating render elements and rendering");
}
@@ -4525,8 +4848,20 @@ impl<W: LayoutElement> Layout<W> {
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if &move_.output == output {
- let location = move_.tile_render_location();
- rv = Some(move_.tile.render(renderer, location, true, target));
+ let scale = Scale::from(move_.output.current_scale().fractional_scale());
+ let zoom = self.overview_zoom();
+ let location = move_.tile_render_location(zoom);
+ let iter = move_
+ .tile
+ .render(renderer, location, true, target)
+ .map(move |elem| {
+ RescaleRenderElement::from_element(
+ elem,
+ location.to_physical_precise_round(scale),
+ zoom,
+ )
+ });
+ rv = Some(iter);
}
}
@@ -4589,7 +4924,7 @@ impl<W: LayoutElement> Layout<W> {
}
} else {
// Cancel the view offset gesture after workspace switches, moves, etc.
- if ws_idx != mon.active_workspace_idx {
+ if !self.overview_open && ws_idx != mon.active_workspace_idx {
ws.view_offset_gesture_end(None);
}
}
@@ -4684,6 +5019,10 @@ impl<W: LayoutElement> Layout<W> {
self.windows().any(|(_, win)| win.id() == window)
}
+ pub fn is_overview_open(&self) -> bool {
+ self.overview_open
+ }
+
fn resolve_scrolling_width(&self, window: &W, width: Option<PresetSize>) -> ColumnWidth {
let width = width.unwrap_or_else(|| PresetSize::Fixed(window.size().w));
match width {
@@ -4709,3 +5048,14 @@ impl<W: LayoutElement> Default for MonitorSet<W> {
Self::NoOutputs { workspaces: vec![] }
}
}
+
+fn compute_overview_zoom(options: &Options, overview_progress: Option<f64>) -> f64 {
+ // Clamp to some sane values.
+ let zoom = options.overview.zoom.0.clamp(0.0001, 0.75);
+
+ if let Some(p) = overview_progress {
+ (1. - p * (1. - zoom)).max(0.0001)
+ } else {
+ 1.
+ }
+}
diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs
index ab5384a8..54f8820f 100644
--- a/src/layout/monitor.rs
+++ b/src/layout/monitor.rs
@@ -5,7 +5,7 @@ use std::time::Duration;
use niri_config::CornerRadius;
use smithay::backend::renderer::element::utils::{
- CropRenderElement, Relocate, RelocateRenderElement,
+ CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Size};
@@ -17,11 +17,12 @@ use super::workspace::{
compute_working_area, OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId,
WorkspaceRenderElement,
};
-use super::{ActivateWindow, HitType, LayoutElement, Options};
+use super::{compute_overview_zoom, ActivateWindow, HitType, LayoutElement, Options};
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::shadow::ShadowRenderElement;
use crate::render_helpers::RenderTarget;
use crate::rubber_band::RubberBand;
use crate::utils::transaction::Transaction;
@@ -68,6 +69,10 @@ pub struct Monitor<W: LayoutElement> {
insert_hint_element: InsertHintElement,
/// Location to rende