diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2025-04-25 09:36:50 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2025-04-25 02:00:18 -0700 |
| commit | af1fca35bb15b8010cd3a12bbafe71b55d9ecf57 (patch) | |
| tree | 6283896fd931b9e5244a435cee9a0c227a850c23 /src | |
| parent | 9571d149b2cecd3df8ba3f90f0af296e9f69af6e (diff) | |
| download | niri-af1fca35bb15b8010cd3a12bbafe71b55d9ecf57.tar.gz niri-af1fca35bb15b8010cd3a12bbafe71b55d9ecf57.tar.bz2 niri-af1fca35bb15b8010cd3a12bbafe71b55d9ecf57.zip | |
Implement an Overview
Diffstat (limited to 'src')
| -rw-r--r-- | src/handlers/xdg_shell.rs | 5 | ||||
| -rw-r--r-- | src/input/mod.rs | 210 | ||||
| -rw-r--r-- | src/input/move_grab.rs | 47 | ||||
| -rw-r--r-- | src/input/spatial_movement_grab.rs | 9 | ||||
| -rw-r--r-- | src/layout/mod.rs | 438 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 236 | ||||
| -rw-r--r-- | src/layout/tests.rs | 20 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 53 | ||||
| -rw-r--r-- | src/niri.rs | 102 |
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 |
