aboutsummaryrefslogtreecommitdiff
path: root/src
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
parent9571d149b2cecd3df8ba3f90f0af296e9f69af6e (diff)
downloadniri-af1fca35bb15b8010cd3a12bbafe71b55d9ecf57.tar.gz
niri-af1fca35bb15b8010cd3a12bbafe71b55d9ecf57.tar.bz2
niri-af1fca35bb15b8010cd3a12bbafe71b55d9ecf57.zip
Implement an Overview
Diffstat (limited to 'src')
-rw-r--r--src/handlers/xdg_shell.rs5
-rw-r--r--src/input/mod.rs210
-rw-r--r--src/input/move_grab.rs47
-rw-r--r--src/input/spatial_movement_grab.rs9
-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
-rw-r--r--src/niri.rs102
9 files changed, 1015 insertions, 105 deletions
diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs
index e06d1fca..4dfce86d 100644
--- a/src/handlers/xdg_shell.rs
+++ b/src/handlers/xdg_shell.rs
@@ -153,7 +153,7 @@ impl XdgShellHandler for State {
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
- let grab = MoveGrab::new(start_data, window);
+ let grab = MoveGrab::new(start_data, window, false);
pointer.set_grab(self, grab, serial, Focus::Clear);
}
PointerOrTouchStartData::Touch(start_data) => {
@@ -316,6 +316,9 @@ impl XdgShellHandler for State {
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
+ // FIXME: somewhere here we probably need to check is_overview_open to match the logic
+ // in update_keyboard_focus().
+
if layers
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
.is_none()
diff --git a/src/input/mod.rs b/src/input/mod.rs
index 94914afc..cc6bff45 100644
--- a/src/input/mod.rs
+++ b/src/input/mod.rs
@@ -394,7 +394,7 @@ impl State {
}
let bindings = &this.niri.config.borrow().binds;
- should_intercept_key(
+ let res = should_intercept_key(
&mut this.niri.suppressed_keys,
bindings,
mod_key,
@@ -406,7 +406,20 @@ impl State {
&this.niri.screenshot_ui,
this.niri.config.borrow().input.disable_power_key_handling,
is_inhibiting_shortcuts,
- )
+ );
+
+ if matches!(res, FilterResult::Forward) {
+ // If we didn't find any bind, try other hardcoded keys.
+ if this.niri.keyboard_focus.is_overview() && pressed {
+ if let Some(bind) = raw.and_then(|raw| hardcoded_overview_bind(raw, *mods))
+ {
+ this.niri.suppressed_keys.insert(key_code);
+ return FilterResult::Intercept(Some(bind));
+ }
+ }
+ }
+
+ res
},
) else {
return;
@@ -1915,6 +1928,20 @@ impl State {
Action::ClearDynamicCastTarget => {
self.set_dynamic_cast_target(CastTarget::Nothing);
}
+ Action::ToggleOverview => {
+ self.niri.layout.toggle_overview();
+ self.niri.queue_redraw_all();
+ }
+ Action::OpenOverview => {
+ if self.niri.layout.open_overview() {
+ self.niri.queue_redraw_all();
+ }
+ }
+ Action::CloseOverview => {
+ if self.niri.layout.close_overview() {
+ self.niri.queue_redraw_all();
+ }
+ }
}
}
@@ -2235,13 +2262,49 @@ impl State {
self.niri.pointer_hidden = false;
self.niri.tablet_cursor_location = None;
+ let is_overview_open = self.niri.layout.is_overview_open();
+
+ if is_overview_open && !pointer.is_grabbed() && button == Some(MouseButton::Right) {
+ if let Some((output, ws)) = self.niri.workspace_under_cursor(true) {
+ let ws_id = ws.id();
+ let ws_idx = self.niri.layout.find_workspace_by_id(ws_id).unwrap().0;
+
+ self.niri.layout.focus_output(&output);
+
+ let location = pointer.current_location();
+ let start_data = PointerGrabStartData {
+ focus: None,
+ button: button_code,
+ location,
+ };
+ self.niri
+ .layout
+ .view_offset_gesture_begin(&output, Some(ws_idx), false);
+ let grab = SpatialMovementGrab::new(start_data, output, ws_id, true);
+ pointer.set_grab(self, grab, serial, Focus::Clear);
+ self.niri
+ .cursor_manager
+ .set_cursor_image(CursorImageStatus::Named(CursorIcon::AllScroll));
+
+ // FIXME: granular.
+ self.niri.queue_redraw_all();
+ return;
+ }
+ }
+
if button == Some(MouseButton::Middle) && !pointer.is_grabbed() {
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
if mod_down {
- let output_ws = self.niri.output_under_cursor().and_then(|output| {
- let mon = self.niri.layout.monitor_for_output(&output)?;
- Some((output, mon.active_workspace_ref()))
- });
+ let output_ws = if is_overview_open {
+ self.niri.workspace_under_cursor(true)
+ } else {
+ // We don't want to accidentally "catch" the wrong workspace during
+ // animations.
+ self.niri.output_under_cursor().and_then(|output| {
+ let mon = self.niri.layout.monitor_for_output(&output)?;
+ Some((output, mon.active_workspace_ref()))
+ })
+ };
if let Some((output, ws)) = output_ws {
let ws_id = ws.id();
@@ -2254,7 +2317,7 @@ impl State {
button: button_code,
location,
};
- let grab = SpatialMovementGrab::new(start_data, output, ws_id);
+ let grab = SpatialMovementGrab::new(start_data, output, ws_id, false);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri
.cursor_manager
@@ -2276,12 +2339,14 @@ impl State {
// Check if we need to start an interactive move.
if button == Some(MouseButton::Left) && !pointer.is_grabbed() {
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
- if mod_down {
+ if is_overview_open || mod_down {
let location = pointer.current_location();
let (output, pos_within_output) = self.niri.output_under(location).unwrap();
let output = output.clone();
- self.niri.layout.activate_window(&window);
+ if !is_overview_open {
+ self.niri.layout.activate_window(&window);
+ }
if self.niri.layout.interactive_move_begin(
window.clone(),
@@ -2293,11 +2358,14 @@ impl State {
button: button_code,
location,
};
- let grab = MoveGrab::new(start_data, window.clone());
+ let grab = MoveGrab::new(start_data, window.clone(), is_overview_open);
pointer.set_grab(self, grab, serial, Focus::Clear);
- self.niri
- .cursor_manager
- .set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
+
+ if !is_overview_open {
+ self.niri
+ .cursor_manager
+ .set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
+ }
}
}
}
@@ -2372,7 +2440,20 @@ impl State {
}
}
- self.niri.layout.activate_window(&window);
+ if !is_overview_open {
+ self.niri.layout.activate_window(&window);
+ }
+
+ // FIXME: granular.
+ self.niri.queue_redraw_all();
+ } else if let Some((output, ws)) = is_overview_open
+ .then(|| self.niri.workspace_under_cursor(false))
+ .flatten()
+ {
+ let ws_idx = self.niri.layout.find_workspace_by_id(ws.id()).unwrap().0;
+
+ self.niri.layout.focus_output(&output);
+ self.niri.layout.toggle_overview_to_workspace(ws_idx);
// FIXME: granular.
self.niri.queue_redraw_all();
@@ -2684,6 +2765,8 @@ impl State {
let tool = self.niri.seat.tablet_seat().get_tool(&event.tool());
if let Some(tool) = tool {
+ let is_overview_open = self.niri.layout.is_overview_open();
+
match event.tip_state() {
TabletToolTipState::Down => {
let serial = SERIAL_COUNTER.next_serial();
@@ -2692,10 +2775,33 @@ impl State {
if let Some(pos) = self.niri.tablet_cursor_location {
let under = self.niri.contents_under(pos);
if let Some((window, _)) = under.window {
+ if let Some(output) = is_overview_open.then_some(under.output).flatten()
+ {
+ let mut workspaces = self.niri.layout.workspaces();
+ if let Some(ws_idx) = workspaces.find_map(|(_, ws_idx, ws)| {
+ ws.windows().any(|w| w.window == window).then_some(ws_idx)
+ }) {
+ drop(workspaces);
+ self.niri.layout.focus_output(&output);
+ self.niri.layout.toggle_overview_to_workspace(ws_idx);
+ }
+ }
+
self.niri.layout.activate_window(&window);
// FIXME: granular.
self.niri.queue_redraw_all();
+ } else if let Some((output, ws)) = is_overview_open
+ .then(|| self.niri.workspace_under(false, pos))
+ .flatten()
+ {
+ let ws_idx = self.niri.layout.find_workspace_by_id(ws.id()).unwrap().0;
+
+ self.niri.layout.focus_output(&output);
+ self.niri.layout.toggle_overview_to_workspace(ws_idx);
+
+ // FIXME: granular.
+ self.niri.queue_redraw_all();
} else if let Some(output) = under.output {
self.niri.layout.focus_output(&output);
@@ -2781,6 +2887,12 @@ impl State {
// We handled this event.
return;
+ } else if event.fingers() == 4 {
+ self.niri.layout.overview_gesture_begin();
+ self.niri.queue_redraw_all();
+
+ // We handled this event.
+ return;
}
let serial = SERIAL_COUNTER.next_serial();
@@ -2816,6 +2928,8 @@ impl State {
delta_y = libinput_event.dy_unaccelerated();
}
+ let uninverted_delta_y = delta_y;
+
let device = event.device();
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
if device.config_scroll_natural_scroll_enabled() {
@@ -2824,6 +2938,8 @@ impl State {
}
}
+ let is_overview_open = self.niri.layout.is_overview_open();
+
if let Some((cx, cy)) = &mut self.niri.gesture_swipe_3f_cumulative {
*cx += delta_x;
*cy += delta_y;
@@ -2835,10 +2951,16 @@ impl State {
if let Some(output) = self.niri.output_under_cursor() {
if cx.abs() > cy.abs() {
- let output_ws = self.niri.output_under_cursor().and_then(|output| {
- let mon = self.niri.layout.monitor_for_output(&output)?;
- Some((output, mon.active_workspace_ref()))
- });
+ let output_ws = if is_overview_open {
+ self.niri.workspace_under_cursor(true)
+ } else {
+ // We don't want to accidentally "catch" the wrong workspace during
+ // animations.
+ self.niri.output_under_cursor().and_then(|output| {
+ let mon = self.niri.layout.monitor_for_output(&output)?;
+ Some((output, mon.active_workspace_ref()))
+ })
+ };
if let Some((output, ws)) = output_ws {
let ws_idx = self.niri.layout.find_workspace_by_id(ws.id()).unwrap().0;
@@ -2880,6 +3002,17 @@ impl State {
handled = true;
}
+ let res = self
+ .niri
+ .layout
+ .overview_gesture_update(-uninverted_delta_y, timestamp);
+ if let Some(redraw) = res {
+ if redraw {
+ self.niri.queue_redraw_all();
+ }
+ handled = true;
+ }
+
if handled {
// We handled this event.
return;
@@ -2916,6 +3049,12 @@ impl State {
handled = true;
}
+ let res = self.niri.layout.overview_gesture_end();
+ if res {
+ self.niri.queue_redraw_all();
+ handled = true;
+ }
+
if handled {
// We handled this event.
return;
@@ -3512,6 +3651,41 @@ fn allowed_during_screenshot(action: &Action) -> bool {
)
}
+fn hardcoded_overview_bind(raw: Keysym, mods: ModifiersState) -> Option<Bind> {
+ let mods = modifiers_from_state(mods);
+ if !mods.is_empty() {
+ return None;
+ }
+
+ let mut repeat = true;
+ let action = match raw {
+ Keysym::Escape | Keysym::Return => {
+ repeat = false;
+ Action::ToggleOverview
+ }
+ Keysym::Left => Action::FocusColumnLeft,
+ Keysym::Right => Action::FocusColumnRight,
+ Keysym::Up => Action::FocusWindowOrWorkspaceUp,
+ Keysym::Down => Action::FocusWindowOrWorkspaceDown,
+ _ => {
+ return None;
+ }
+ };
+
+ Some(Bind {
+ key: Key {
+ trigger: Trigger::Keysym(raw),
+ modifiers: Modifiers::empty(),
+ },
+ action,
+ repeat,
+ cooldown: None,
+ allow_when_locked: false,
+ allow_inhibiting: false,
+ hotkey_overlay_title: None,
+ })
+}
+
pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::Device) {
// According to Mutter code, this setting is specific to touchpads.
let is_touchpad = device.config_tap_finger_count() > 0;
diff --git a/src/input/move_grab.rs b/src/input/move_grab.rs
index 96362b45..e939696b 100644
--- a/src/input/move_grab.rs
+++ b/src/input/move_grab.rs
@@ -1,10 +1,11 @@
use smithay::backend::input::ButtonState;
use smithay::desktop::Window;
use smithay::input::pointer::{
- AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
- GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
- GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
- MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
+ AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
+ GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
+ GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
+ GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
+ RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
@@ -15,14 +16,32 @@ pub struct MoveGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
+ gesture: GestureState,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum GestureState {
+ Recognizing,
+ Move,
}
impl MoveGrab {
- pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
+ pub fn new(
+ start_data: PointerGrabStartData<State>,
+ window: Window,
+ use_threshold: bool,
+ ) -> Self {
+ let gesture = if use_threshold {
+ GestureState::Recognizing
+ } else {
+ GestureState::Move
+ };
+
Self {
last_location: start_data.location,
start_data,
window,
+ gesture,
}
}
@@ -53,6 +72,24 @@ impl PointerGrab<State> for MoveGrab {
let output = output.clone();
let event_delta = event.location - self.last_location;
self.last_location = event.location;
+
+ if self.gesture == GestureState::Recognizing {
+ let c = event.location - self.start_data.location;
+
+ // Check if the gesture moved far enough to decide.
+ if c.x * c.x + c.y * c.y >= 8. * 8. {
+ self.gesture = GestureState::Move;
+
+ data.niri
+ .cursor_manager
+ .set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
+ }
+ }
+
+ if self.gesture != GestureState::Move {
+ return;
+ }
+
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,
diff --git a/src/input/spatial_movement_grab.rs b/src/input/spatial_movement_grab.rs
index 6eec7b0a..e536866c 100644
--- a/src/input/spatial_movement_grab.rs
+++ b/src/input/spatial_movement_grab.rs
@@ -33,13 +33,20 @@ impl SpatialMovementGrab {
start_data: PointerGrabStartData<State>,
output: Output,
workspace_id: WorkspaceId,
+ is_view_offset: bool,
) -> Self {
+ let gesture = if is_view_offset {
+ GestureState::ViewOffset
+ } else {
+ GestureState::Recognizing
+ };
+
Self {
last_location: start_data.location,
start_data,
output,
workspace_id,
- gesture: GestureState::Recognizing,
+ gesture,
}
}
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