Implement an Overview

This commit is contained in:
Ivan Molodetskikh
2025-04-25 09:36:50 +03:00
parent 9571d149b2
commit af1fca35bb
11 changed files with 1094 additions and 105 deletions
+73
View File
@@ -62,6 +62,8 @@ pub struct Config {
#[knuffel(child, default)] #[knuffel(child, default)]
pub gestures: Gestures, pub gestures: Gestures,
#[knuffel(child, default)] #[knuffel(child, default)]
pub overview: Overview,
#[knuffel(child, default)]
pub environment: Environment, pub environment: Environment,
#[knuffel(children(name = "window-rule"))] #[knuffel(children(name = "window-rule"))]
pub window_rules: Vec<WindowRule>, pub window_rules: Vec<WindowRule>,
@@ -988,6 +990,8 @@ pub struct Animations {
pub config_notification_open_close: ConfigNotificationOpenCloseAnim, pub config_notification_open_close: ConfigNotificationOpenCloseAnim,
#[knuffel(child, default)] #[knuffel(child, default)]
pub screenshot_ui_open: ScreenshotUiOpenAnim, pub screenshot_ui_open: ScreenshotUiOpenAnim,
#[knuffel(child, default)]
pub overview_open_close: OverviewOpenCloseAnim,
} }
impl Default for Animations { impl Default for Animations {
@@ -1003,6 +1007,7 @@ impl Default for Animations {
window_resize: Default::default(), window_resize: Default::default(),
config_notification_open_close: Default::default(), config_notification_open_close: Default::default(),
screenshot_ui_open: Default::default(), screenshot_ui_open: Default::default(),
overview_open_close: Default::default(),
} }
} }
} }
@@ -1150,6 +1155,22 @@ impl Default for ScreenshotUiOpenAnim {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OverviewOpenCloseAnim(pub Animation);
impl Default for OverviewOpenCloseAnim {
fn default() -> Self {
Self(Animation {
off: false,
kind: AnimationKind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 800,
epsilon: 0.0001,
}),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Animation { pub struct Animation {
pub off: bool, pub off: bool,
@@ -1209,6 +1230,20 @@ impl Default for DndEdgeViewScroll {
} }
} }
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct Overview {
#[knuffel(child, unwrap(argument), default = Self::default().zoom)]
pub zoom: FloatOrInt<0, 1>,
}
impl Default for Overview {
fn default() -> Self {
Self {
zoom: FloatOrInt(0.5),
}
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq, Eq)] #[derive(knuffel::Decode, Debug, Default, Clone, PartialEq, Eq)]
pub struct Environment(#[knuffel(children)] pub Vec<EnvironmentVariable>); pub struct Environment(#[knuffel(children)] pub Vec<EnvironmentVariable>);
@@ -1720,6 +1755,9 @@ pub enum Action {
SetDynamicCastWindowById(u64), SetDynamicCastWindowById(u64),
SetDynamicCastMonitor(#[knuffel(argument)] Option<String>), SetDynamicCastMonitor(#[knuffel(argument)] Option<String>),
ClearDynamicCastTarget, ClearDynamicCastTarget,
ToggleOverview,
OpenOverview,
CloseOverview,
} }
impl From<niri_ipc::Action> for Action { impl From<niri_ipc::Action> for Action {
@@ -1984,6 +2022,9 @@ impl From<niri_ipc::Action> for Action {
Self::SetDynamicCastMonitor(output) Self::SetDynamicCastMonitor(output)
} }
niri_ipc::Action::ClearDynamicCastTarget {} => Self::ClearDynamicCastTarget, niri_ipc::Action::ClearDynamicCastTarget {} => Self::ClearDynamicCastTarget,
niri_ipc::Action::ToggleOverview {} => Self::ToggleOverview,
niri_ipc::Action::OpenOverview {} => Self::OpenOverview,
niri_ipc::Action::CloseOverview {} => Self::CloseOverview,
} }
} }
} }
@@ -2968,6 +3009,21 @@ where
} }
} }
impl<S> knuffel::Decode<S> for OverviewOpenCloseAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().0;
Ok(Self(Animation::decode_node(node, ctx, default, |_, _| {
Ok(false)
})?))
}
}
impl Animation { impl Animation {
pub fn new_off() -> Self { pub fn new_off() -> Self {
Self { Self {
@@ -4469,6 +4525,18 @@ mod tests {
), ),
}, },
), ),
overview_open_close: OverviewOpenCloseAnim(
Animation {
off: false,
kind: Spring(
SpringParams {
damping_ratio: 1.0,
stiffness: 800,
epsilon: 0.0001,
},
),
},
),
}, },
gestures: Gestures { gestures: Gestures {
dnd_edge_view_scroll: DndEdgeViewScroll { dnd_edge_view_scroll: DndEdgeViewScroll {
@@ -4481,6 +4549,11 @@ mod tests {
), ),
}, },
}, },
overview: Overview {
zoom: FloatOrInt(
0.5,
),
},
environment: Environment( environment: Environment(
[ [
EnvironmentVariable { EnvironmentVariable {
+6
View File
@@ -764,6 +764,12 @@ pub enum Action {
}, },
/// Clear the dynamic cast target, making it show nothing. /// Clear the dynamic cast target, making it show nothing.
ClearDynamicCastTarget {}, ClearDynamicCastTarget {},
/// Toggle (open/close) the Overview.
ToggleOverview {},
/// Open the Overview.
OpenOverview {},
/// Close the Overview.
CloseOverview {},
} }
/// Change in window or column size. /// Change in window or column size.
+4 -1
View File
@@ -153,7 +153,7 @@ impl XdgShellHandler for State {
match start_data { match start_data {
PointerOrTouchStartData::Pointer(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); pointer.set_grab(self, grab, serial, Focus::Clear);
} }
PointerOrTouchStartData::Touch(start_data) => { PointerOrTouchStartData::Touch(start_data) => {
@@ -316,6 +316,9 @@ impl XdgShellHandler for State {
} else if let Some(output) = self.niri.layout.active_output() { } else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(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 if layers
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL) .layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
.is_none() .is_none()
+192 -18
View File
@@ -394,7 +394,7 @@ impl State {
} }
let bindings = &this.niri.config.borrow().binds; let bindings = &this.niri.config.borrow().binds;
should_intercept_key( let res = should_intercept_key(
&mut this.niri.suppressed_keys, &mut this.niri.suppressed_keys,
bindings, bindings,
mod_key, mod_key,
@@ -406,7 +406,20 @@ impl State {
&this.niri.screenshot_ui, &this.niri.screenshot_ui,
this.niri.config.borrow().input.disable_power_key_handling, this.niri.config.borrow().input.disable_power_key_handling,
is_inhibiting_shortcuts, 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 { ) else {
return; return;
@@ -1915,6 +1928,20 @@ impl State {
Action::ClearDynamicCastTarget => { Action::ClearDynamicCastTarget => {
self.set_dynamic_cast_target(CastTarget::Nothing); 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.pointer_hidden = false;
self.niri.tablet_cursor_location = None; 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() { if button == Some(MouseButton::Middle) && !pointer.is_grabbed() {
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers()); let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
if mod_down { if mod_down {
let output_ws = self.niri.output_under_cursor().and_then(|output| { let output_ws = if is_overview_open {
let mon = self.niri.layout.monitor_for_output(&output)?; self.niri.workspace_under_cursor(true)
Some((output, mon.active_workspace_ref())) } 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 { if let Some((output, ws)) = output_ws {
let ws_id = ws.id(); let ws_id = ws.id();
@@ -2254,7 +2317,7 @@ impl State {
button: button_code, button: button_code,
location, 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); pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri self.niri
.cursor_manager .cursor_manager
@@ -2276,12 +2339,14 @@ impl State {
// Check if we need to start an interactive move. // Check if we need to start an interactive move.
if button == Some(MouseButton::Left) && !pointer.is_grabbed() { if button == Some(MouseButton::Left) && !pointer.is_grabbed() {
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers()); 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 location = pointer.current_location();
let (output, pos_within_output) = self.niri.output_under(location).unwrap(); let (output, pos_within_output) = self.niri.output_under(location).unwrap();
let output = output.clone(); 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( if self.niri.layout.interactive_move_begin(
window.clone(), window.clone(),
@@ -2293,11 +2358,14 @@ impl State {
button: button_code, button: button_code,
location, 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); pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri
.cursor_manager if !is_overview_open {
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move)); 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. // FIXME: granular.
self.niri.queue_redraw_all(); self.niri.queue_redraw_all();
@@ -2684,6 +2765,8 @@ impl State {
let tool = self.niri.seat.tablet_seat().get_tool(&event.tool()); let tool = self.niri.seat.tablet_seat().get_tool(&event.tool());
if let Some(tool) = tool { if let Some(tool) = tool {
let is_overview_open = self.niri.layout.is_overview_open();
match event.tip_state() { match event.tip_state() {
TabletToolTipState::Down => { TabletToolTipState::Down => {
let serial = SERIAL_COUNTER.next_serial(); let serial = SERIAL_COUNTER.next_serial();
@@ -2692,8 +2775,31 @@ impl State {
if let Some(pos) = self.niri.tablet_cursor_location { if let Some(pos) = self.niri.tablet_cursor_location {
let under = self.niri.contents_under(pos); let under = self.niri.contents_under(pos);
if let Some((window, _)) = under.window { 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); 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. // FIXME: granular.
self.niri.queue_redraw_all(); self.niri.queue_redraw_all();
} else if let Some(output) = under.output { } else if let Some(output) = under.output {
@@ -2779,6 +2885,12 @@ impl State {
if event.fingers() == 3 { if event.fingers() == 3 {
self.niri.gesture_swipe_3f_cumulative = Some((0., 0.)); self.niri.gesture_swipe_3f_cumulative = Some((0., 0.));
// 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. // We handled this event.
return; return;
} }
@@ -2816,6 +2928,8 @@ impl State {
delta_y = libinput_event.dy_unaccelerated(); delta_y = libinput_event.dy_unaccelerated();
} }
let uninverted_delta_y = delta_y;
let device = event.device(); let device = event.device();
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() { if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
if device.config_scroll_natural_scroll_enabled() { 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 { if let Some((cx, cy)) = &mut self.niri.gesture_swipe_3f_cumulative {
*cx += delta_x; *cx += delta_x;
*cy += delta_y; *cy += delta_y;
@@ -2835,10 +2951,16 @@ impl State {
if let Some(output) = self.niri.output_under_cursor() { if let Some(output) = self.niri.output_under_cursor() {
if cx.abs() > cy.abs() { if cx.abs() > cy.abs() {
let output_ws = self.niri.output_under_cursor().and_then(|output| { let output_ws = if is_overview_open {
let mon = self.niri.layout.monitor_for_output(&output)?; self.niri.workspace_under_cursor(true)
Some((output, mon.active_workspace_ref())) } 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 { if let Some((output, ws)) = output_ws {
let ws_idx = self.niri.layout.find_workspace_by_id(ws.id()).unwrap().0; let ws_idx = self.niri.layout.find_workspace_by_id(ws.id()).unwrap().0;
@@ -2880,6 +3002,17 @@ impl State {
handled = true; 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 { if handled {
// We handled this event. // We handled this event.
return; return;
@@ -2916,6 +3049,12 @@ impl State {
handled = true; handled = true;
} }
let res = self.niri.layout.overview_gesture_end();
if res {
self.niri.queue_redraw_all();
handled = true;
}
if handled { if handled {
// We handled this event. // We handled this event.
return; 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) { pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::Device) {
// According to Mutter code, this setting is specific to touchpads. // According to Mutter code, this setting is specific to touchpads.
let is_touchpad = device.config_tap_finger_count() > 0; let is_touchpad = device.config_tap_finger_count() > 0;
+42 -5
View File
@@ -1,10 +1,11 @@
use smithay::backend::input::ButtonState; use smithay::backend::input::ButtonState;
use smithay::desktop::Window; use smithay::desktop::Window;
use smithay::input::pointer::{ use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent, AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent, GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
RelativeMotionEvent,
}; };
use smithay::input::SeatHandler; use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point}; use smithay::utils::{IsAlive, Logical, Point};
@@ -15,14 +16,32 @@ pub struct MoveGrab {
start_data: PointerGrabStartData<State>, start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>, last_location: Point<f64, Logical>,
window: Window, window: Window,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GestureState {
Recognizing,
Move,
} }
impl MoveGrab { 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 { Self {
last_location: start_data.location, last_location: start_data.location,
start_data, start_data,
window, window,
gesture,
} }
} }
@@ -53,6 +72,24 @@ impl PointerGrab<State> for MoveGrab {
let output = output.clone(); let output = output.clone();
let event_delta = event.location - self.last_location; let event_delta = event.location - self.last_location;
self.last_location = event.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( let ongoing = data.niri.layout.interactive_move_update(
&self.window, &self.window,
event_delta, event_delta,
+8 -1
View File
@@ -33,13 +33,20 @@ impl SpatialMovementGrab {
start_data: PointerGrabStartData<State>, start_data: PointerGrabStartData<State>,
output: Output, output: Output,
workspace_id: WorkspaceId, workspace_id: WorkspaceId,
is_view_offset: bool,
) -> Self { ) -> Self {
let gesture = if is_view_offset {
GestureState::ViewOffset
} else {
GestureState::Recognizing
};
Self { Self {
last_location: start_data.location, last_location: start_data.location,
start_data, start_data,
output, output,
workspace_id, workspace_id,
gesture: GestureState::Recognizing, gesture,
} }
} }
+394 -44
View File
@@ -45,6 +45,7 @@ use niri_config::{
use niri_ipc::{ColumnDisplay, PositionChange, SizeChange}; use niri_ipc::{ColumnDisplay, PositionChange, SizeChange};
use scrolling::{Column, ColumnWidth}; use scrolling::{Column, ColumnWidth};
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::utils::RescaleRenderElement;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::output::{self, Output}; use smithay::output::{self, Output};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@@ -55,7 +56,8 @@ use workspace::{WorkspaceAddWindowTarget, WorkspaceId};
pub use self::monitor::MonitorRenderElement; pub use self::monitor::MonitorRenderElement;
use self::monitor::{Monitor, WorkspaceSwitch}; use self::monitor::{Monitor, WorkspaceSwitch};
use self::workspace::{OutputId, Workspace}; 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::layout::scrolling::ScrollDirection;
use crate::niri_render_elements; use crate::niri_render_elements;
use crate::render_helpers::offscreen::OffscreenData; 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. /// Opacity of interactively moved tiles targeting the scrolling layout.
const INTERACTIVE_MOVE_ALPHA: f64 = 0.75; 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. /// Size-relative units.
pub struct SizeFrac; pub struct SizeFrac;
@@ -293,6 +303,13 @@ pub struct Layout<W: LayoutElement> {
clock: Clock, clock: Clock,
/// Time that we last updated render elements for. /// Time that we last updated render elements for.
update_render_elements_time: Duration, 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. /// Configurable properties of the layout.
options: Rc<Options>, options: Rc<Options>,
} }
@@ -338,6 +355,7 @@ pub struct Options {
pub preset_window_heights: Vec<PresetSize>, pub preset_window_heights: Vec<PresetSize>,
pub animations: niri_config::Animations, pub animations: niri_config::Animations,
pub gestures: niri_config::Gestures, pub gestures: niri_config::Gestures,
pub overview: niri_config::Overview,
// Debug flags. // Debug flags.
pub disable_resize_throttling: bool, pub disable_resize_throttling: bool,
pub disable_transactions: bool, pub disable_transactions: bool,
@@ -365,6 +383,7 @@ impl Default for Options {
default_column_width: None, default_column_width: None,
animations: Default::default(), animations: Default::default(),
gestures: Default::default(), gestures: Default::default(),
overview: Default::default(),
disable_resize_throttling: false, disable_resize_throttling: false,
disable_transactions: false, disable_transactions: false,
preset_window_heights: vec![ 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> { impl<W: LayoutElement> InteractiveMoveState<W> {
fn moving(&self) -> Option<&InteractiveMoveData<W>> { fn moving(&self) -> Option<&InteractiveMoveData<W>> {
match self { match self {
@@ -510,16 +544,16 @@ impl<W: LayoutElement> InteractiveMoveState<W> {
} }
impl<W: LayoutElement> InteractiveMoveData<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 scale = Scale::from(self.output.current_scale().fractional_scale());
let window_size = self.tile.window_size(); let window_size = self.tile.window_size();
let pointer_offset_within_window = Point::from(( let pointer_offset_within_window = Point::from((
window_size.w * self.pointer_ratio_within_window.0, window_size.w * self.pointer_ratio_within_window.0,
window_size.h * self.pointer_ratio_within_window.1, window_size.h * self.pointer_ratio_within_window.1,
)); ));
let pos = let pos = self.pointer_pos_within_output
self.pointer_pos_within_output - pointer_offset_within_window - self.tile.window_loc() - (pointer_offset_within_window + self.tile.window_loc() - self.tile.render_offset())
+ self.tile.render_offset(); .upscale(zoom);
// Round to physical pixels. // Round to physical pixels.
pos.to_physical_precise_round(scale).to_logical(scale) pos.to_physical_precise_round(scale).to_logical(scale)
} }
@@ -553,6 +587,15 @@ impl HitType {
tile.hit(pos_within_tile) tile.hit(pos_within_tile)
.map(|hit| (tile.window(), hit.offset_win_pos(tile_pos))) .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 { impl Options {
@@ -594,6 +637,7 @@ impl Options {
default_column_width, default_column_width,
animations: config.animations.clone(), animations: config.animations.clone(),
gestures: config.gestures, gestures: config.gestures,
overview: config.overview,
disable_resize_throttling: config.debug.disable_resize_throttling, disable_resize_throttling: config.debug.disable_resize_throttling,
disable_transactions: config.debug.disable_transactions, disable_transactions: config.debug.disable_transactions,
preset_window_heights, 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> { impl<W: LayoutElement> Layout<W> {
pub fn new(clock: Clock, config: &Config) -> Self { pub fn new(clock: Clock, config: &Config) -> Self {
Self::with_options_and_workspaces(clock, config, Options::from_config(config)) Self::with_options_and_workspaces(clock, config, Options::from_config(config))
@@ -625,6 +682,8 @@ impl<W: LayoutElement> Layout<W> {
dnd: None, dnd: None,
clock, clock,
update_render_elements_time: Duration::ZERO, update_render_elements_time: Duration::ZERO,
overview_open: false,
overview_progress: None,
options: Rc::new(options), options: Rc::new(options),
} }
} }
@@ -648,6 +707,8 @@ impl<W: LayoutElement> Layout<W> {
dnd: None, dnd: None,
clock, clock,
update_render_elements_time: Duration::ZERO, update_render_elements_time: Duration::ZERO,
overview_open: false,
overview_progress: None,
options: opts, options: opts,
} }
} }
@@ -751,6 +812,8 @@ impl<W: LayoutElement> Layout<W> {
let mut monitor = let mut monitor =
Monitor::new(output, workspaces, self.clock.clone(), self.options.clone()); Monitor::new(output, workspaces, self.clock.clone(), self.options.clone());
monitor.active_workspace_idx = active_workspace_idx; 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); monitors.push(monitor);
MonitorSet::Normal { MonitorSet::Normal {
@@ -789,6 +852,8 @@ impl<W: LayoutElement> Layout<W> {
let mut monitor = let mut monitor =
Monitor::new(output, workspaces, self.clock.clone(), self.options.clone()); Monitor::new(output, workspaces, self.clock.clone(), self.options.clone());
monitor.active_workspace_idx = active_workspace_idx; monitor.active_workspace_idx = active_workspace_idx;
monitor.overview_open = self.overview_open;
monitor.set_overview_progress(self.overview_progress.as_ref());
MonitorSet::Normal { MonitorSet::Normal {
monitors: vec![monitor], monitors: vec![monitor],
@@ -1418,7 +1483,7 @@ impl<W: LayoutElement> Layout<W> {
let mut target = Rectangle::from_size(Size::from((width, height))); let mut target = Rectangle::from_size(Size::from((width, height)));
// FIXME: ideally this shouldn't include the tile render offset, but the code // FIXME: ideally this shouldn't include the tile render offset, but the code
// duplication would be a bit annoying for this edge case. // 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; target.loc.y -= move_.tile.window_loc().y;
return target; return target;
} }
@@ -2279,8 +2344,19 @@ impl<W: LayoutElement> Layout<W> {
) -> Option<(&W, HitType)> { ) -> Option<(&W, HitType)> {
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.output == *output { if move_.output == *output {
let tile_pos = move_.tile_render_location(); if self.overview_progress.is_some() {
HitType::hit_tile(&move_.tile, tile_pos, pos_within_output) 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 { } else {
None None
} }
@@ -2316,6 +2392,36 @@ impl<W: LayoutElement> Layout<W> {
mon.resize_edges_under(pos_within_output) 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)] #[cfg(test)]
fn verify_invariants(&self) { fn verify_invariants(&self) {
use std::collections::HashSet; use std::collections::HashSet;
@@ -2324,6 +2430,8 @@ impl<W: LayoutElement> Layout<W> {
use crate::layout::monitor::WorkspaceSwitch; use crate::layout::monitor::WorkspaceSwitch;
let zoom = self.overview_zoom();
let mut move_win_id = None; let mut move_win_id = None;
if let Some(state) = &self.interactive_move { if let Some(state) = &self.interactive_move {
match state { match state {
@@ -2352,7 +2460,7 @@ impl<W: LayoutElement> Layout<W> {
base options adjusted for output scale" 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); let rounded_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale);
// Tile position must be rounded to physical pixels. // Tile position must be rounded to physical pixels.
@@ -2460,6 +2568,12 @@ impl<W: LayoutElement> Layout<W> {
"monitor options must be synchronized with layout" "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 { if let Some(WorkspaceSwitch::Animation(anim)) = &monitor.workspace_switch {
let before_idx = anim.from() as usize; let before_idx = anim.from() as usize;
let after_idx = anim.to() as usize; let after_idx = anim.to() as usize;
@@ -2650,6 +2764,8 @@ impl<W: LayoutElement> Layout<W> {
// Scroll the view if needed. // Scroll the view if needed.
if let Some((output, pos_within_output)) = dnd_scroll { if let Some((output, pos_within_output)) = dnd_scroll {
if let Some(mon) = self.monitor_for_output_mut(&output) { 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) { if let Some((ws, geo)) = mon.workspace_under(pos_within_output) {
let ws_id = ws.id(); let ws_id = ws.id();
let ws = mon let ws = mon
@@ -2657,7 +2773,18 @@ impl<W: LayoutElement> Layout<W> {
.iter_mut() .iter_mut()
.find(|ws| ws.id() == ws_id) .find(|ws| ws.id() == ws_id)
.unwrap(); .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 { match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => { MonitorSet::Normal { monitors, .. } => {
for mon in monitors { for mon in monitors {
mon.set_overview_progress(self.overview_progress.as_ref());
mon.advance_animations(); 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 { let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return false; return false;
}; };
@@ -2719,9 +2855,10 @@ impl<W: LayoutElement> Layout<W> {
self.update_render_elements_time = self.clock.now(); self.update_render_elements_time = self.clock.now();
let zoom = self.overview_zoom();
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if output.map_or(true, |output| move_.output == *output) { 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 = let view_rect =
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output)); Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output));
move_.tile.update_render_elements(true, view_rect); move_.tile.update_render_elements(true, view_rect);
@@ -2745,6 +2882,7 @@ impl<W: LayoutElement> Layout<W> {
let is_active = self.is_active let is_active = self.is_active
&& idx == *active_monitor_idx && idx == *active_monitor_idx
&& !matches!(self.interactive_move, Some(InteractiveMoveState::Moving(_))); && !matches!(self.interactive_move, Some(InteractiveMoveState::Moving(_)));
mon.set_overview_progress(self.overview_progress.as_ref());
mon.update_render_elements(is_active); 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"); let _span = tracy_client::span!("Layout::update_insert_hint::update");
if let Some(mon) = self.monitor_for_output_mut(&move_.output) { 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) { if let Some((ws, geo)) = mon.workspace_under(move_.pointer_pos_within_output) {
let ws_id = ws.id(); let ws_id = ws.id();
let ws = mon let ws = mon
@@ -2805,8 +2944,8 @@ impl<W: LayoutElement> Layout<W> {
.iter_mut() .iter_mut()
.find(|ws| ws.id() == ws_id) .find(|ws| ws.id() == ws_id)
.unwrap(); .unwrap();
let pos_within_workspace =
let pos_within_workspace = move_.pointer_pos_within_output - geo.loc; (move_.pointer_pos_within_output - geo.loc).downscale(zoom);
let position = ws.scrolling_insert_position(pos_within_workspace); let position = ws.scrolling_insert_position(pos_within_workspace);
let rules = move_.tile.window().rules(); let rules = move_.tile.window().rules();
@@ -3645,6 +3784,9 @@ impl<W: LayoutElement> Layout<W> {
timestamp: Duration, timestamp: Duration,
is_touchpad: bool, is_touchpad: bool,
) -> Option<Option<Output>> { ) -> Option<Option<Output>> {
let zoom = self.overview_zoom();
let delta_x = delta_x / zoom;
let monitors = match &mut self.monitor_set { let monitors = match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => monitors, MonitorSet::Normal { monitors, .. } => monitors,
MonitorSet::NoOutputs { .. } => return None, MonitorSet::NoOutputs { .. } => return None,
@@ -3684,6 +3826,77 @@ impl<W: LayoutElement> Layout<W> {
None 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( pub fn interactive_move_begin(
&mut self, &mut self,
window_id: W::Id, window_id: W::Id,
@@ -3710,6 +3923,8 @@ impl<W: LayoutElement> Layout<W> {
return false; return false;
} }
let zoom = mon.overview_zoom();
let is_floating = ws.is_floating(&window_id); let is_floating = ws.is_floating(&window_id);
let (tile, tile_offset, _visible) = ws let (tile, tile_offset, _visible) = ws
.tiles_with_render_positions() .tiles_with_render_positions()
@@ -3717,10 +3932,11 @@ impl<W: LayoutElement> Layout<W> {
.unwrap(); .unwrap();
let window_offset = tile.window_loc(); 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 pointer_offset_within_window =
let window_size = tile.window_size(); start_pos_within_output - tile_pos - window_offset.upscale(zoom);
let window_size = tile.window_size().upscale(zoom);
let pointer_ratio_within_window = ( let pointer_ratio_within_window = (
f64::clamp(pointer_offset_within_window.x / window_size.w, 0., 1.), f64::clamp(pointer_offset_within_window.x / window_size.w, 0., 1.),
f64::clamp(pointer_offset_within_window.y / window_size.h, 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; return false;
} }
let zoom = self.overview_zoom();
let delta = delta.downscale(zoom);
pointer_delta += delta; pointer_delta += delta;
let (cx, cy) = (pointer_delta.x, pointer_delta.y); let (cx, cy) = (pointer_delta.x, pointer_delta.y);
@@ -3824,7 +4043,8 @@ impl<W: LayoutElement> Layout<W> {
.find(|(tile, _, _)| tile.window().id() == window) .find(|(tile, _, _)| tile.window().id() == window)
.unwrap(); .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, pointer_ratio_within_window,
}; };
if let Some(tile_pos) = tile_pos { if let Some((tile_pos, zoom)) = tile_pos {
let new_tile_pos = data.tile_render_location(); let new_tile_pos = data.tile_render_location(zoom);
data.tile.animate_move_from(tile_pos - new_tile_pos); data.tile
.animate_move_from((tile_pos - new_tile_pos).downscale(zoom));
} }
self.interactive_move = Some(InteractiveMoveState::Moving(data)); self.interactive_move = Some(InteractiveMoveState::Moving(data));
@@ -3960,12 +4181,16 @@ impl<W: LayoutElement> Layout<W> {
unreachable!() unreachable!()
}; };
let mut ws_id = None;
for ws in self.workspaces_mut() { for ws in self.workspaces_mut() {
let id = ws.id();
if let Some(tile) = ws.tiles_mut().find(|tile| *tile.window().id() == window_id) if let Some(tile) = ws.tiles_mut().find(|tile| *tile.window().id() == window_id)
{ {
let offset = tile.interactive_move_offset; let offset = tile.interactive_move_offset;
tile.interactive_move_offset = Point::from((0., 0.)); tile.interactive_move_offset = Point::from((0., 0.));
tile.animate_move_from(offset); tile.animate_move_from(offset);
ws_id = Some(id);
} }
// Unlock the view on the workspaces, but if the moved window was active, // 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; return;
} }
InteractiveMoveState::Moving(move_) => move_, 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 { match &mut self.monitor_set {
MonitorSet::Normal { MonitorSet::Normal {
monitors, monitors,
active_monitor_idx, 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) monitors.iter_mut().find(|mon| mon.output == move_.output)
{ {
let zoom = mon.overview_zoom();
let (ws, ws_geo) = mon let (ws, ws_geo) = mon
.workspace_under(move_.pointer_pos_within_output) .workspace_under(move_.pointer_pos_within_output)
// If the pointer is somehow outside the move output and a workspace switch // 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 // is in progress, this won't necessarily do the expected thing.
// that is not really supposed to happen so eh?
.unwrap_or_else(|| mon.workspaces_with_render_geo().next().unwrap()); .unwrap_or_else(|| mon.workspaces_with_render_geo().next().unwrap());
let ws_id = ws.id(); let ws_id = ws.id();
@@ -4033,14 +4292,16 @@ impl<W: LayoutElement> Layout<W> {
let position = if move_.is_floating { let position = if move_.is_floating {
InsertPosition::Floating InsertPosition::Floating
} else { } 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]; let ws = &mut mon.workspaces[ws_idx];
ws.scrolling_insert_position(pos_within_workspace) ws.scrolling_insert_position(pos_within_workspace)
}; };
(mon, ws_idx, position, ws_geo.loc) (mon, ws_idx, position, ws_geo.loc, zoom)
} else { } else {
let mon = &mut monitors[*active_monitor_idx]; 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. // 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 (ws, ws_geo) = mon.workspaces_with_render_geo().next().unwrap();
@@ -4056,11 +4317,12 @@ impl<W: LayoutElement> Layout<W> {
.iter_mut() .iter_mut()
.position(|ws| ws.id() == ws_id) .position(|ws| ws.id() == ws_id)
.unwrap(); .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 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 { match position {
InsertPosition::NewColumn(column_idx) => { InsertPosition::NewColumn(column_idx) => {
@@ -4071,7 +4333,7 @@ impl<W: LayoutElement> Layout<W> {
id: ws_id, id: ws_id,
column_idx: Some(column_idx), column_idx: Some(column_idx),
}, },
ActivateWindow::Yes, activate,
move_.width, move_.width,
move_.is_full_width, move_.is_full_width,
false, false,
@@ -4083,13 +4345,15 @@ impl<W: LayoutElement> Layout<W> {
column_idx, column_idx,
Some(tile_idx), Some(tile_idx),
move_.tile, move_.tile,
true, activate == ActivateWindow::Yes,
); );
} }
InsertPosition::Floating => { InsertPosition::Floating => {
let pos = move_.tile_render_location() - offset; let tile_render_loc = move_.tile_render_location(zoom);
let mut tile = move_.tile; 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); let pos = mon.workspaces[ws_idx].floating_logical_to_size_frac(pos);
tile.floating_pos = Some(pos); tile.floating_pos = Some(pos);
@@ -4106,7 +4370,7 @@ impl<W: LayoutElement> Layout<W> {
id: ws_id, id: ws_id,
column_idx: None, column_idx: None,
}, },
ActivateWindow::Yes, activate,
move_.width, move_.width,
move_.is_full_width, move_.is_full_width,
true, true,
@@ -4115,15 +4379,18 @@ impl<W: LayoutElement> Layout<W> {
} }
// needed because empty_workspace_above_first could have modified the idx // needed because empty_workspace_above_first could have modified the idx
let ws_idx = mon.active_workspace_idx(); let (tile, tile_render_loc, ws_geo) = mon
let ws = &mut mon.workspaces[ws_idx]; .workspaces_with_render_geo_mut()
let (tile, tile_render_loc) = ws .find_map(|(ws, geo)| {
.tiles_with_render_positions_mut(false) ws.tiles_with_render_positions_mut(false)
.find(|(tile, _)| tile.window().id() == &win_id) .find(|(tile, _)| tile.window().id() == &win_id)
.map(|(tile, tile_render_loc)| (tile, tile_render_loc, geo))
})
.unwrap(); .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, .. } => { MonitorSet::NoOutputs { workspaces, .. } => {
if workspaces.is_empty() { if workspaces.is_empty() {
@@ -4138,7 +4405,7 @@ impl<W: LayoutElement> Layout<W> {
ws.add_tile( ws.add_tile(
move_.tile, move_.tile,
WorkspaceAddWindowTarget::Auto, WorkspaceAddWindowTarget::Auto,
ActivateWindow::Yes, activate,
move_.width, move_.width,
move_.is_full_width, move_.is_full_width,
move_.is_floating, move_.is_floating,
@@ -4376,6 +4643,60 @@ impl<W: LayoutElement> Layout<W> {
self.unname_workspace_by_id(id); 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) { pub fn start_open_animation_for_window(&mut self, window: &W::Id) {
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().id() == window { 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 _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 let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if move_.tile.window().id() == window { if move_.tile.window().id() == window {
let Some(snapshot) = move_.tile.take_unmap_snapshot() else { let Some(snapshot) = move_.tile.take_unmap_snapshot() else {
return; return;
}; };
let tile_pos = move_.tile_render_location(); let tile_pos = move_.tile_render_location(zoom);
let tile_size = move_.tile.tile_size(); let tile_size = move_.tile.tile_size();
let output = move_.output.clone(); let output = move_.output.clone();
@@ -4516,7 +4839,7 @@ impl<W: LayoutElement> Layout<W> {
renderer: &mut R, renderer: &mut R,
output: &Output, output: &Output,
target: RenderTarget, target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> + 'a { ) -> impl Iterator<Item = RescaleRenderElement<TileRenderElement<R>>> + 'a {
if self.update_render_elements_time != self.clock.now() { if self.update_render_elements_time != self.clock.now() {
error!("clock moved between updating render elements and rendering"); 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 let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if &move_.output == output { if &move_.output == output {
let location = move_.tile_render_location(); let scale = Scale::from(move_.output.current_scale().fractional_scale());
rv = Some(move_.tile.render(renderer, location, true, target)); 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 { } else {
// Cancel the view offset gesture after workspace switches, moves, etc. // 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); ws.view_offset_gesture_end(None);
} }
} }
@@ -4684,6 +5019,10 @@ impl<W: LayoutElement> Layout<W> {
self.windows().any(|(_, win)| win.id() == window) 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 { fn resolve_scrolling_width(&self, window: &W, width: Option<PresetSize>) -> ColumnWidth {
let width = width.unwrap_or_else(|| PresetSize::Fixed(window.size().w)); let width = width.unwrap_or_else(|| PresetSize::Fixed(window.size().w));
match width { match width {
@@ -4709,3 +5048,14 @@ impl<W: LayoutElement> Default for MonitorSet<W> {
Self::NoOutputs { workspaces: vec![] } 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.
}
}
+216 -20
View File
@@ -5,7 +5,7 @@ use std::time::Duration;
use niri_config::CornerRadius; use niri_config::CornerRadius;
use smithay::backend::renderer::element::utils::{ use smithay::backend::renderer::element::utils::{
CropRenderElement, Relocate, RelocateRenderElement, CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement,
}; };
use smithay::output::Output; use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Size}; use smithay::utils::{Logical, Point, Rectangle, Size};
@@ -17,11 +17,12 @@ use super::workspace::{
compute_working_area, OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId, compute_working_area, OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId,
WorkspaceRenderElement, WorkspaceRenderElement,
}; };
use super::{ActivateWindow, HitType, LayoutElement, Options}; use super::{compute_overview_zoom, ActivateWindow, HitType, LayoutElement, Options};
use crate::animation::{Animation, Clock}; use crate::animation::{Animation, Clock};
use crate::input::swipe_tracker::SwipeTracker; use crate::input::swipe_tracker::SwipeTracker;
use crate::niri_render_elements; use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::RenderTarget; use crate::render_helpers::RenderTarget;
use crate::rubber_band::RubberBand; use crate::rubber_band::RubberBand;
use crate::utils::transaction::Transaction; use crate::utils::transaction::Transaction;
@@ -68,6 +69,10 @@ pub struct Monitor<W: LayoutElement> {
insert_hint_element: InsertHintElement, insert_hint_element: InsertHintElement,
/// Location to render the insert hint element. /// Location to render the insert hint element.
insert_hint_render_loc: Option<InsertHintRenderLoc>, insert_hint_render_loc: Option<InsertHintRenderLoc>,
/// Whether the overview is open.
pub(super) overview_open: bool,
/// Progress of the overview zoom animation, 1 is fully in overview.
overview_progress: Option<OverviewProgress>,
/// Clock for driving animations. /// Clock for driving animations.
pub(super) clock: Clock, pub(super) clock: Clock,
/// Configurable properties of the layout. /// Configurable properties of the layout.
@@ -94,6 +99,8 @@ pub struct WorkspaceSwitchGesture {
tracker: SwipeTracker, tracker: SwipeTracker,
/// Whether the gesture is controlled by the touchpad. /// Whether the gesture is controlled by the touchpad.
is_touchpad: bool, is_touchpad: bool,
/// Whether the gesture is clamped to +-1 workspace around the center.
is_clamped: bool,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -116,6 +123,12 @@ struct InsertHintRenderLoc {
location: Point<f64, Logical>, location: Point<f64, Logical>,
} }
#[derive(Debug)]
pub(super) enum OverviewProgress {
Animation(Animation),
Value(f64),
}
/// Where to put a newly added window. /// Where to put a newly added window.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum MonitorAddWindowTarget<'a, W: LayoutElement> { pub enum MonitorAddWindowTarget<'a, W: LayoutElement> {
@@ -137,10 +150,12 @@ niri_render_elements! {
MonitorInnerRenderElement<R> => { MonitorInnerRenderElement<R> => {
Workspace = CropRenderElement<WorkspaceRenderElement<R>>, Workspace = CropRenderElement<WorkspaceRenderElement<R>>,
InsertHint = CropRenderElement<InsertHintRenderElement>, InsertHint = CropRenderElement<InsertHintRenderElement>,
Shadow = ShadowRenderElement,
} }
} }
pub type MonitorRenderElement<R> = RelocateRenderElement<MonitorInnerRenderElement<R>>; pub type MonitorRenderElement<R> =
RelocateRenderElement<RescaleRenderElement<MonitorInnerRenderElement<R>>>;
impl WorkspaceSwitch { impl WorkspaceSwitch {
pub fn current_idx(&self) -> f64 { pub fn current_idx(&self) -> f64 {
@@ -183,9 +198,38 @@ impl WorkspaceSwitch {
impl WorkspaceSwitchGesture { impl WorkspaceSwitchGesture {
fn min_max(&self, workspace_count: usize) -> (f64, f64) { fn min_max(&self, workspace_count: usize) -> (f64, f64) {
let min = self.center_idx.saturating_sub(1) as f64; if self.is_clamped {
let max = (self.center_idx + 1).min(workspace_count - 1) as f64; let min = self.center_idx.saturating_sub(1) as f64;
(min, max) let max = (self.center_idx + 1).min(workspace_count - 1) as f64;
(min, max)
} else {
(0., (workspace_count - 1) as f64)
}
}
}
impl OverviewProgress {
pub fn value(&self) -> f64 {
match self {
OverviewProgress::Animation(anim) => anim.value(),
OverviewProgress::Value(v) => *v,
}
}
pub fn clamped_value(&self) -> f64 {
match self {
OverviewProgress::Animation(anim) => anim.clamped_value(),
OverviewProgress::Value(v) => *v,
}
}
}
impl From<&super::OverviewProgress> for OverviewProgress {
fn from(value: &super::OverviewProgress) -> Self {
match value {
super::OverviewProgress::Animation(anim) => Self::Animation(anim.clone()),
super::OverviewProgress::Gesture(gesture) => Self::Value(gesture.value),
}
} }
} }
@@ -212,6 +256,8 @@ impl<W: LayoutElement> Monitor<W> {
insert_hint: None, insert_hint: None,
insert_hint_element: InsertHintElement::new(options.insert_hint), insert_hint_element: InsertHintElement::new(options.insert_hint),
insert_hint_render_loc: None, insert_hint_render_loc: None,
overview_open: false,
overview_progress: None,
workspace_switch: None, workspace_switch: None,
clock, clock,
options, options,
@@ -769,7 +815,9 @@ impl<W: LayoutElement> Monitor<W> {
// Make sure the hint is at least partially visible. // Make sure the hint is at least partially visible.
if matches!(hint.position, InsertPosition::NewColumn(_)) { if matches!(hint.position, InsertPosition::NewColumn(_)) {
let zoom = self.overview_zoom();
let geo = insert_hint_ws_geo.unwrap(); 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.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.); area.loc.x = area.loc.x.min(geo.loc.x + geo.size.w - area.size.w / 2.);
@@ -942,6 +990,10 @@ impl<W: LayoutElement> Monitor<W> {
/// ///
/// During animations, assumes the final view position. /// During animations, assumes the final view position.
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> { pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
if self.overview_open {
return None;
}
self.active_workspace_ref().active_tile_visual_rectangle() self.active_workspace_ref().active_tile_visual_rectangle()
} }
@@ -962,7 +1014,100 @@ impl<W: LayoutElement> Monitor<W> {
self.workspace_size(zoom) + Size::from((0., gap)) self.workspace_size(zoom) + Size::from((0., gap))
} }
pub fn overview_zoom(&self) -> f64 {
let progress = self.overview_progress.as_ref().map(|p| p.value());
compute_overview_zoom(&self.options, progress)
}
pub(super) fn set_overview_progress(&mut self, progress: Option<&super::OverviewProgress>) {
let prev_render_idx = self.workspace_render_idx();
self.overview_progress = progress.map(OverviewProgress::from);
let new_render_idx = self.workspace_render_idx();
// If the view jumped (can happen when going from corrected to uncorrected render_idx, for
// example when toggling the overview in the middle of an overview animation), then restart
// the workspace switch to avoid jumps.
if prev_render_idx != new_render_idx {
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
// FIXME: maintain velocity.
*anim = anim.restarted(prev_render_idx, anim.to(), 0.);
}
}
}
#[cfg(test)]
pub(super) fn overview_progress_value(&self) -> Option<f64> {
self.overview_progress.as_ref().map(|p| p.value())
}
pub fn workspace_render_idx(&self) -> f64 { pub fn workspace_render_idx(&self) -> f64 {
// If workspace switch and overview progress are matching animations, then compute a
// correction term to make the movement appear monotonic.
if let (
Some(WorkspaceSwitch::Animation(switch_anim)),
Some(OverviewProgress::Animation(progress_anim)),
) = (&self.workspace_switch, &self.overview_progress)
{
if switch_anim.start_time() == progress_anim.start_time()
&& (switch_anim.duration().as_secs_f64() - progress_anim.duration().as_secs_f64())
.abs()
<= 0.001
{
#[rustfmt::skip]
// How this was derived:
//
// - Assume we're animating a zoom + switch. Consider switch "from" and "to".
// These are render_idx values, so first workspace to second would have switch
// from = 0. and to = 1. regardless of the zoom level.
//
// - At the start, the point at "from" is at Y = 0. We're moving the point at "to"
// to Y = 0. We want this to be a monotonic motion in apparent coordinates (after
// zoom).
//
// - Height at the start:
// from_height = (size.h + gap) * from_zoom.
//
// - Current height:
// current_height = (size.h + gap) * zoom.
//
// - We're moving the "to" point to Y = 0:
// to_y = 0.
//
// - The initial position of the point we're moving:
// from_y = (to - from) * from_height.
//
// - We want this point to travel monotonically in apparent coordinates:
// current_y = from_y + (to_y - from_y) * progress,
// where progress is from 0 to 1, equals to the animation progress (switch and
// zoom are the same since they are synchronized).
//
// - Derive the Y of the first workspace from this:
// first_y = current_y - to * current_height.
//
// Now, let's substitute and rearrange the terms.
//
// - current_y = from_y + (0 - (to - from) * from_height) * progress
// - progress = (switch_anim.value() - from) / (to - from)
// - current_y = from_y - (to - from) * from_height * (switch_anim.value() - from) / (to - from)
// - current_y = from_y - from_height * (switch_anim.value() - from)
// - first_y = from_y - from_height * (switch_anim.value() - from) - to * current_height
// - first_y = (to - from) * from_height - from_height * (switch_anim.value() - from) - to * current_height
// - first_y = to * from_height - switch_anim.value() * from_height - to * current_height
// - first_y = -switch_anim.value() * from_height + to * (from_height - current_height)
let from = progress_anim.from();
let from_zoom = compute_overview_zoom(&self.options, Some(from));
let from_ws_height_with_gap = self.workspace_size_with_gap(from_zoom).h;
let zoom = self.overview_zoom();
let ws_height_with_gap = self.workspace_size_with_gap(zoom).h;
let first_ws_y = -switch_anim.value() * from_ws_height_with_gap
+ switch_anim.to() * (from_ws_height_with_gap - ws_height_with_gap);
return -first_ws_y / ws_height_with_gap;
}
};
if let Some(switch) = &self.workspace_switch { if let Some(switch) = &self.workspace_switch {
switch.current_idx() switch.current_idx()
} else { } else {
@@ -972,19 +1117,24 @@ impl<W: LayoutElement> Monitor<W> {
pub fn workspaces_render_geo(&self) -> impl Iterator<Item = Rectangle<f64, Logical>> { pub fn workspaces_render_geo(&self) -> impl Iterator<Item = Rectangle<f64, Logical>> {
let scale = self.scale.fractional_scale(); let scale = self.scale.fractional_scale();
let zoom = 1.; let zoom = self.overview_zoom();
let ws_size = self.workspace_size(zoom); let ws_size = self.workspace_size(zoom);
let gap = self.workspace_gap(zoom); let gap = self.workspace_gap(zoom);
let ws_height_with_gap = ws_size.h + gap; let ws_height_with_gap = ws_size.h + gap;
let static_offset = (self.view_size.to_point() - ws_size.to_point()).downscale(2.);
let static_offset = static_offset
.to_physical_precise_round(scale)
.to_logical(scale);
let first_ws_y = -self.workspace_render_idx() * ws_height_with_gap; let first_ws_y = -self.workspace_render_idx() * ws_height_with_gap;
let first_ws_y = round_logical_in_physical(scale, first_ws_y); let first_ws_y = round_logical_in_physical(scale, first_ws_y);
// Return position for one-past-last workspace too. // Return position for one-past-last workspace too.
(0..=self.workspaces.len()).map(move |idx| { (0..=self.workspaces.len()).map(move |idx| {
let y = first_ws_y + idx as f64 * ws_height_with_gap; let y = first_ws_y + idx as f64 * ws_height_with_gap;
let loc = Point::from((0., y)); let loc = Point::from((0., y)) + static_offset;
Rectangle::new(loc, ws_size) Rectangle::new(loc, ws_size)
}) })
} }
@@ -1026,20 +1176,42 @@ impl<W: LayoutElement> Monitor<W> {
Some((ws, geo)) Some((ws, geo))
} }
pub fn workspace_under_narrow(
&self,
pos_within_output: Point<f64, Logical>,
) -> Option<&Workspace<W>> {
self.workspaces_with_render_geo()
.find_map(|(ws, geo)| geo.contains(pos_within_output).then_some(ws))
}
pub fn window_under(&self, pos_within_output: Point<f64, Logical>) -> Option<(&W, HitType)> { pub fn window_under(&self, pos_within_output: Point<f64, Logical>) -> Option<(&W, HitType)> {
let (ws, geo) = self.workspace_under(pos_within_output)?; let (ws, geo) = self.workspace_under(pos_within_output)?;
let (win, hit) = ws.window_under(pos_within_output - geo.loc)?;
Some((win, hit.offset_win_pos(geo.loc))) if self.overview_progress.is_some() {
let zoom = self.overview_zoom();
let pos_within_workspace = (pos_within_output - geo.loc).downscale(zoom);
let (win, hit) = ws.window_under(pos_within_workspace)?;
// During the overview animation, we cannot do input hits because we cannot really
// represent scaled windows properly.
Some((win, hit.to_activate()))
} else {
let (win, hit) = ws.window_under(pos_within_output - geo.loc)?;
Some((win, hit.offset_win_pos(geo.loc)))
}
} }
pub fn resize_edges_under(&self, pos_within_output: Point<f64, Logical>) -> Option<ResizeEdge> { pub fn resize_edges_under(&self, pos_within_output: Point<f64, Logical>) -> Option<ResizeEdge> {
if self.overview_progress.is_some() {
return None;
}
let (ws, geo) = self.workspace_under(pos_within_output)?; let (ws, geo) = self.workspace_under(pos_within_output)?;
ws.resize_edges_under(pos_within_output - geo.loc) ws.resize_edges_under(pos_within_output - geo.loc)
} }
pub fn render_above_top_layer(&self) -> bool { pub fn render_above_top_layer(&self) -> bool {
// Render above the top layer only if the view is stationary. // Render above the top layer only if the view is stationary.
if self.workspace_switch.is_some() { if self.workspace_switch.is_some() || self.overview_progress.is_some() {
return false; return false;
} }
@@ -1074,7 +1246,7 @@ impl<W: LayoutElement> Monitor<W> {
// rendering for maximized GTK windows. // rendering for maximized GTK windows.
// //
// FIXME: use proper bounds after fixing the Crop element. // FIXME: use proper bounds after fixing the Crop element.
let crop_bounds = if self.workspace_switch.is_some() { let crop_bounds = if self.workspace_switch.is_some() || self.overview_progress.is_some() {
Rectangle::new( Rectangle::new(
Point::from((-i32::MAX / 2, 0)), Point::from((-i32::MAX / 2, 0)),
Size::from((i32::MAX, height)), Size::from((i32::MAX, height)),
@@ -1086,6 +1258,9 @@ impl<W: LayoutElement> Monitor<W> {
) )
}; };
let zoom = self.overview_zoom();
let overview_clamped_progress = self.overview_progress.as_ref().map(|p| p.clamped_value());
// Draw the insert hint. // Draw the insert hint.
let mut insert_hint = None; let mut insert_hint = None;
if !self.options.insert_hint.off { if !self.options.insert_hint.off {
@@ -1109,6 +1284,13 @@ impl<W: LayoutElement> Monitor<W> {
let floating = floating.filter_map(map_ws_contents); let floating = floating.filter_map(map_ws_contents);
let scrolling = scrolling.filter_map(map_ws_contents); let scrolling = scrolling.filter_map(map_ws_contents);
let shadow = overview_clamped_progress.map(|value| {
ws.render_shadow(renderer)
.map(move |elem| elem.with_alpha(value.clamp(0., 1.) as f32))
.map(MonitorInnerRenderElement::Shadow)
});
let shadow = shadow.into_iter().flatten();
let hint = if matches!(insert_hint, Some((hint_ws_id, _)) if hint_ws_id == ws.id()) { let hint = if matches!(insert_hint, Some((hint_ws_id, _)) if hint_ws_id == ws.id()) {
let iter = insert_hint.take().unwrap().1; let iter = insert_hint.take().unwrap().1;
let iter = iter.filter_map(move |elem| { let iter = iter.filter_map(move |elem| {
@@ -1122,9 +1304,10 @@ impl<W: LayoutElement> Monitor<W> {
}; };
let hint = hint.into_iter().flatten(); let hint = hint.into_iter().flatten();
let iter = floating.chain(hint).chain(scrolling); let iter = floating.chain(hint).chain(scrolling).chain(shadow);
let iter = iter.map(move |elem| { let iter = iter.map(move |elem| {
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), zoom);
RelocateRenderElement::from_element( RelocateRenderElement::from_element(
elem, elem,
// The offset we get from workspaces_with_render_positions() is already // The offset we get from workspaces_with_render_positions() is already
@@ -1149,6 +1332,7 @@ impl<W: LayoutElement> Monitor<W> {
current_idx, current_idx,
tracker: SwipeTracker::new(), tracker: SwipeTracker::new(),
is_touchpad, is_touchpad,
is_clamped: !self.overview_open,
}; };
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture)); self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
} }
@@ -1167,6 +1351,7 @@ impl<W: LayoutElement> Monitor<W> {
return None; return None;
} }
let zoom = self.overview_zoom();
let total_height = if gesture.is_touchpad { let total_height = if gesture.is_touchpad {
WORKSPACE_GESTURE_MOVEMENT WORKSPACE_GESTURE_MOVEMENT
} else { } else {
@@ -1177,13 +1362,24 @@ impl<W: LayoutElement> Monitor<W> {
return None; return None;
}; };
// Reduce the effect of zoom on the touchpad somewhat.
let delta_scale = if gesture.is_touchpad {
(zoom - 1.) / 2.5 + 1.
} else {
zoom
};
let delta_y = delta_y / delta_scale;
let mut rubber_band = WORKSPACE_GESTURE_RUBBER_BAND;
rubber_band.limit /= zoom;
gesture.tracker.push(delta_y, timestamp); gesture.tracker.push(delta_y, timestamp);
let pos = gesture.tracker.pos() / total_height; let pos = gesture.tracker.pos() / total_height;
let (min, max) = gesture.min_max(self.workspaces.len()); let (min, max) = gesture.min_max(self.workspaces.len());
let new_idx = gesture.start_idx + pos; let new_idx = gesture.start_idx + pos;
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx); let new_idx = rubber_band.clamp(min, max, new_idx);
if gesture.current_idx == new_idx { if gesture.current_idx == new_idx {
return Some(false); return Some(false);
@@ -1202,6 +1398,7 @@ impl<W: LayoutElement> Monitor<W> {
return false; return false;
} }
let zoom = self.overview_zoom();
let total_height = if gesture.is_touchpad { let total_height = if gesture.is_touchpad {
WORKSPACE_GESTURE_MOVEMENT WORKSPACE_GESTURE_MOVEMENT
} else { } else {
@@ -1216,6 +1413,9 @@ impl<W: LayoutElement> Monitor<W> {
let now = self.clock.now_unadjusted(); let now = self.clock.now_unadjusted();
gesture.tracker.push(0., now); gesture.tracker.push(0., now);
let mut rubber_band = WORKSPACE_GESTURE_RUBBER_BAND;
rubber_band.limit /= zoom;
let mut velocity = gesture.tracker.velocity() / total_height; let mut velocity = gesture.tracker.velocity() / total_height;
let current_pos = gesture.tracker.pos() / total_height; let current_pos = gesture.tracker.pos() / total_height;
let pos = gesture.tracker.projected_end_pos() / total_height; let pos = gesture.tracker.projected_end_pos() / total_height;
@@ -1223,14 +1423,10 @@ impl<W: LayoutElement> Monitor<W> {
let (min, max) = gesture.min_max(self.workspaces.len()); let (min, max) = gesture.min_max(self.workspaces.len());
let new_idx = gesture.start_idx + pos; let new_idx = gesture.start_idx + pos;
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx); let new_idx = new_idx.clamp(min, max);
let new_idx = new_idx.round() as usize; let new_idx = new_idx.round() as usize;
velocity *= WORKSPACE_GESTURE_RUBBER_BAND.clamp_derivative( velocity *= rubber_band.clamp_derivative(min, max, gesture.start_idx + current_pos);
min,
max,
gesture.start_idx + current_pos,
);
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id()); self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
+20
View File
@@ -603,6 +603,13 @@ enum Op {
WorkspaceSwitchGestureEnd { WorkspaceSwitchGestureEnd {
is_touchpad: Option<bool>, is_touchpad: Option<bool>,
}, },
OverviewGestureBegin,
OverviewGestureUpdate {
#[proptest(strategy = "-400f64..400f64")]
delta: f64,
timestamp: Duration,
},
OverviewGestureEnd,
InteractiveMoveBegin { InteractiveMoveBegin {
#[proptest(strategy = "1..=5usize")] #[proptest(strategy = "1..=5usize")]
window: usize, window: usize,
@@ -658,6 +665,7 @@ enum Op {
#[proptest(strategy = "1..=5usize")] #[proptest(strategy = "1..=5usize")]
window: usize, window: usize,
}, },
ToggleOverview,
} }
impl Op { impl Op {
@@ -1387,6 +1395,15 @@ impl Op {
Op::WorkspaceSwitchGestureEnd { is_touchpad } => { Op::WorkspaceSwitchGestureEnd { is_touchpad } => {
layout.workspace_switch_gesture_end(is_touchpad); layout.workspace_switch_gesture_end(is_touchpad);
} }
Op::OverviewGestureBegin => {
layout.overview_gesture_begin();
}
Op::OverviewGestureUpdate { delta, timestamp } => {
layout.overview_gesture_update(delta, timestamp);
}
Op::OverviewGestureEnd => {
layout.overview_gesture_end();
}
Op::InteractiveMoveBegin { Op::InteractiveMoveBegin {
window, window,
output_idx, output_idx,
@@ -1440,6 +1457,9 @@ impl Op {
Op::InteractiveResizeEnd { window } => { Op::InteractiveResizeEnd { window } => {
layout.interactive_resize_end(&window); layout.interactive_resize_end(&window);
} }
Op::ToggleOverview => {
layout.toggle_overview();
}
} }
} }
} }
+51 -2
View File
@@ -2,7 +2,10 @@ use std::cmp::max;
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use niri_config::{CenterFocusedColumn, OutputName, PresetSize, Workspace as WorkspaceConfig}; use niri_config::{
CenterFocusedColumn, CornerRadius, FloatOrInt, OutputName, PresetSize,
Workspace as WorkspaceConfig,
};
use niri_ipc::{ColumnDisplay, PositionChange, SizeChange}; use niri_ipc::{ColumnDisplay, PositionChange, SizeChange};
use smithay::backend::renderer::gles::GlesRenderer; use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::{layer_map_for_output, Window}; use smithay::desktop::{layer_map_for_output, Window};
@@ -17,6 +20,7 @@ use super::floating::{FloatingSpace, FloatingSpaceRenderElement};
use super::scrolling::{ use super::scrolling::{
Column, ColumnWidth, ScrollDirection, ScrollingSpace, ScrollingSpaceRenderElement, Column, ColumnWidth, ScrollDirection, ScrollingSpace, ScrollingSpaceRenderElement,
}; };
use super::shadow::Shadow;
use super::tile::{Tile, TileRenderSnapshot}; use super::tile::{Tile, TileRenderSnapshot};
use super::{ use super::{
ActivateWindow, HitType, InsertPosition, InteractiveResizeData, LayoutElement, Options, ActivateWindow, HitType, InsertPosition, InteractiveResizeData, LayoutElement, Options,
@@ -25,6 +29,7 @@ use super::{
use crate::animation::Clock; use crate::animation::Clock;
use crate::niri_render_elements; use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::RenderTarget; use crate::render_helpers::RenderTarget;
use crate::utils::id::IdCounter; use crate::utils::id::IdCounter;
use crate::utils::transaction::{Transaction, TransactionBlocker}; use crate::utils::transaction::{Transaction, TransactionBlocker};
@@ -80,6 +85,9 @@ pub struct Workspace<W: LayoutElement> {
/// zones. /// zones.
working_area: Rectangle<f64, Logical>, working_area: Rectangle<f64, Logical>,
/// This workspace's shadow in the overview.
shadow: Shadow,
/// Clock for driving animations. /// Clock for driving animations.
pub(super) clock: Clock, pub(super) clock: Clock,
@@ -228,6 +236,17 @@ impl<W: LayoutElement> Workspace<W> {
options.clone(), options.clone(),
); );
let shadow_config = niri_config::Shadow {
on: true,
offset: niri_config::ShadowOffset {
x: FloatOrInt(0.),
y: FloatOrInt(20.),
},
softness: FloatOrInt(120.),
spread: FloatOrInt(20.),
..Default::default()
};
Self { Self {
scrolling, scrolling,
floating, floating,
@@ -237,6 +256,7 @@ impl<W: LayoutElement> Workspace<W> {
transform: output.current_transform(), transform: output.current_transform(),
view_size, view_size,
working_area, working_area,
shadow: Shadow::new(shadow_config),
output: Some(output), output: Some(output),
clock, clock,
base_options, base_options,
@@ -281,6 +301,17 @@ impl<W: LayoutElement> Workspace<W> {
options.clone(), options.clone(),
); );
let shadow_config = niri_config::Shadow {
on: true,
offset: niri_config::ShadowOffset {
x: FloatOrInt(0.),
y: FloatOrInt(20.),
},
softness: FloatOrInt(120.),
spread: FloatOrInt(20.),
..Default::default()
};
Self { Self {
scrolling, scrolling,
floating, floating,
@@ -291,6 +322,7 @@ impl<W: LayoutElement> Workspace<W> {
original_output, original_output,
view_size, view_size,
working_area, working_area,
shadow: Shadow::new(shadow_config),
clock, clock,
base_options, base_options,
options, options,
@@ -343,6 +375,14 @@ impl<W: LayoutElement> Workspace<W> {
let view_rect = Rectangle::from_size(self.view_size); let view_rect = Rectangle::from_size(self.view_size);
self.floating self.floating
.update_render_elements(is_active && self.floating_is_active.get(), view_rect); .update_render_elements(is_active && self.floating_is_active.get(), view_rect);
self.shadow.update_render_elements(
self.view_size,
true,
CornerRadius::default(),
self.scale.fractional_scale(),
1.,
);
} }
pub fn update_config(&mut self, base_options: Rc<Options>) { pub fn update_config(&mut self, base_options: Rc<Options>) {
@@ -370,6 +410,7 @@ impl<W: LayoutElement> Workspace<W> {
pub fn update_shaders(&mut self) { pub fn update_shaders(&mut self) {
self.scrolling.update_shaders(); self.scrolling.update_shaders();
self.floating.update_shaders(); self.floating.update_shaders();
self.shadow.update_shaders();
} }
pub fn windows(&self) -> impl Iterator<Item = &W> + '_ { pub fn windows(&self) -> impl Iterator<Item = &W> + '_ {
@@ -1432,6 +1473,13 @@ impl<W: LayoutElement> Workspace<W> {
(floating, scrolling) (floating, scrolling)
} }
pub fn render_shadow<R: NiriRenderer>(
&self,
renderer: &mut R,
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
self.shadow.render(renderer, Point::from((0., 0.)))
}
pub fn render_above_top_layer(&self) -> bool { pub fn render_above_top_layer(&self) -> bool {
self.scrolling.render_above_top_layer() self.scrolling.render_above_top_layer()
} }
@@ -1630,7 +1678,7 @@ impl<W: LayoutElement> Workspace<W> {
self.scrolling.dnd_scroll_gesture_begin(); self.scrolling.dnd_scroll_gesture_begin();
} }
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>) { pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>, speed: f64) {
let config = &self.options.gestures.dnd_edge_view_scroll; let config = &self.options.gestures.dnd_edge_view_scroll;
let trigger_width = config.trigger_width.0; let trigger_width = config.trigger_width.0;
@@ -1656,6 +1704,7 @@ impl<W: LayoutElement> Workspace<W> {
// Normalize to [0, 1]. // Normalize to [0, 1].
delta / trigger_width delta / trigger_width
}; };
let delta = delta * speed;
self.scrolling.dnd_scroll_gesture_scroll(delta); self.scrolling.dnd_scroll_gesture_scroll(delta);
} }
+88 -14
View File
@@ -133,7 +133,7 @@ use crate::ipc::server::IpcServer;
use crate::layer::mapped::LayerSurfaceRenderElement; use crate::layer::mapped::LayerSurfaceRenderElement;
use crate::layer::MappedLayer; use crate::layer::MappedLayer;
use crate::layout::tile::TileRenderElement; use crate::layout::tile::TileRenderElement;
use crate::layout::workspace::WorkspaceId; use crate::layout::workspace::{Workspace, WorkspaceId};
use crate::layout::{HitType, Layout, LayoutElement as _, MonitorRenderElement}; use crate::layout::{HitType, Layout, LayoutElement as _, MonitorRenderElement};
use crate::niri_render_elements; use crate::niri_render_elements;
use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState}; use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
@@ -470,6 +470,7 @@ pub enum KeyboardFocus {
LayerShell { surface: WlSurface }, LayerShell { surface: WlSurface },
LockScreen { surface: Option<WlSurface> }, LockScreen { surface: Option<WlSurface> },
ScreenshotUi, ScreenshotUi,
Overview,
} }
#[derive(Default, Clone, PartialEq)] #[derive(Default, Clone, PartialEq)]
@@ -566,6 +567,7 @@ impl KeyboardFocus {
KeyboardFocus::LayerShell { surface } => Some(surface), KeyboardFocus::LayerShell { surface } => Some(surface),
KeyboardFocus::LockScreen { surface } => surface.as_ref(), KeyboardFocus::LockScreen { surface } => surface.as_ref(),
KeyboardFocus::ScreenshotUi => None, KeyboardFocus::ScreenshotUi => None,
KeyboardFocus::Overview => None,
} }
} }
@@ -575,12 +577,17 @@ impl KeyboardFocus {
KeyboardFocus::LayerShell { surface } => Some(surface), KeyboardFocus::LayerShell { surface } => Some(surface),
KeyboardFocus::LockScreen { surface } => surface, KeyboardFocus::LockScreen { surface } => surface,
KeyboardFocus::ScreenshotUi => None, KeyboardFocus::ScreenshotUi => None,
KeyboardFocus::Overview => None,
} }
} }
pub fn is_layout(&self) -> bool { pub fn is_layout(&self) -> bool {
matches!(self, KeyboardFocus::Layout { .. }) matches!(self, KeyboardFocus::Layout { .. })
} }
pub fn is_overview(&self) -> bool {
matches!(self, KeyboardFocus::Overview)
}
} }
pub struct State { pub struct State {
@@ -1026,13 +1033,18 @@ impl State {
let focus_on_layer = let focus_on_layer =
|layer| excl_focus_on_layer(layer).or_else(|| on_d_focus_on_layer(layer)); |layer| excl_focus_on_layer(layer).or_else(|| on_d_focus_on_layer(layer));
let is_overview_open = self.niri.layout.is_overview_open();
let mut surface = grab_on_layer(Layer::Overlay); let mut surface = grab_on_layer(Layer::Overlay);
// FIXME: we shouldn't prioritize the top layer grabs over regular overlay input or a // FIXME: we shouldn't prioritize the top layer grabs over regular overlay input or a
// fullscreen layout window. This will need tracking in grab() to avoid handing it out // fullscreen layout window. This will need tracking in grab() to avoid handing it out
// in the first place. Or a better way to structure this code. // in the first place. Or a better way to structure this code.
surface = surface.or_else(|| grab_on_layer(Layer::Top)); surface = surface.or_else(|| grab_on_layer(Layer::Top));
surface = surface.or_else(|| grab_on_layer(Layer::Bottom));
surface = surface.or_else(|| grab_on_layer(Layer::Background)); if !is_overview_open {
surface = surface.or_else(|| grab_on_layer(Layer::Bottom));
surface = surface.or_else(|| grab_on_layer(Layer::Background));
}
surface = surface.or_else(|| focus_on_layer(Layer::Overlay)); surface = surface.or_else(|| focus_on_layer(Layer::Overlay));
@@ -1043,6 +1055,11 @@ impl State {
surface = surface.or_else(|| focus_on_layer(Layer::Background)); surface = surface.or_else(|| focus_on_layer(Layer::Background));
} else { } else {
surface = surface.or_else(|| focus_on_layer(Layer::Top)); surface = surface.or_else(|| focus_on_layer(Layer::Top));
if is_overview_open {
surface = Some(surface.unwrap_or(KeyboardFocus::Overview));
}
surface = surface.or_else(|| on_d_focus_on_layer(Layer::Bottom)); surface = surface.or_else(|| on_d_focus_on_layer(Layer::Bottom));
surface = surface.or_else(|| on_d_focus_on_layer(Layer::Background)); surface = surface.or_else(|| on_d_focus_on_layer(Layer::Background));
surface = surface.or_else(layout_focus); surface = surface.or_else(layout_focus);
@@ -1102,7 +1119,9 @@ impl State {
// focused window. // focused window.
// //
// FIXME: Ideally this should happen inside Layout itself, then there wouldn't be any // FIXME: Ideally this should happen inside Layout itself, then there wouldn't be any
// problems with layer-shell, etc. // problems with layer-shell, etc. Or a similar problem now with the Overview where we
// don't update the previously focused window because the keyboard focus is on the
// Overview rather than on the Layout.
if matches!(self.niri.keyboard_focus, KeyboardFocus::Layout { .. }) if matches!(self.niri.keyboard_focus, KeyboardFocus::Layout { .. })
&& matches!(focus, KeyboardFocus::Layout { .. }) && matches!(focus, KeyboardFocus::Layout { .. })
{ {
@@ -2910,6 +2929,10 @@ impl Niri {
output: &Output, output: &Output,
pos_within_output: Point<f64, Logical>, pos_within_output: Point<f64, Logical>,
) -> bool { ) -> bool {
if self.layout.is_overview_open() {
return false;
}
// Check if some layer-shell surface is on top. // Check if some layer-shell surface is on top.
let layers = layer_map_for_output(output); let layers = layer_map_for_output(output);
let layer_popup_under = |layer| { let layer_popup_under = |layer| {
@@ -2939,6 +2962,42 @@ impl Niri {
false false
} }
/// Returns the workspace under the position to be activated.
///
/// The return value is an output and a workspace index on it.
pub fn workspace_under(
&self,
extended_bounds: bool,
pos: Point<f64, Logical>,
) -> Option<(Output, &Workspace<Mapped>)> {
if self.is_locked() || self.screenshot_ui.is_open() {
return None;
}
let (output, pos_within_output) = self.output_under(pos)?;
if self.is_sticky_obscured_under(output, pos_within_output) {
return None;
}
if self.is_layout_obscured_under(output, pos_within_output) {
return None;
}
let ws = self
.layout
.workspace_under(extended_bounds, output, pos_within_output)?;
Some((output.clone(), ws))
}
pub fn workspace_under_cursor(
&self,
extended_bounds: bool,
) -> Option<(Output, &Workspace<Mapped>)> {
let pos = self.seat.get_pointer().unwrap().current_location();
self.workspace_under(extended_bounds, pos)
}
/// Returns the window under the position to be activated. /// Returns the window under the position to be activated.
/// ///
/// The cursor may be inside the window's activation region, but not within the window's input /// The cursor may be inside the window's activation region, but not within the window's input
@@ -3039,6 +3098,8 @@ impl Niri {
let mon = self.layout.monitor_for_output(output)?; let mon = self.layout.monitor_for_output(output)?;
let (_, geo) = mon.workspace_under(pos_within_output)?; let (_, geo) = mon.workspace_under(pos_within_output)?;
layer_pos_within_output += geo.loc; layer_pos_within_output += geo.loc;
// Don't need to deal with zoom here because in the overview background and
// bottom layers don't receive input.
} }
let surface_type = if popup { let surface_type = if popup {
@@ -3096,6 +3157,8 @@ impl Niri {
let mut under = let mut under =
layer_popup_under(Layer::Overlay).or_else(|| layer_toplevel_under(Layer::Overlay)); layer_popup_under(Layer::Overlay).or_else(|| layer_toplevel_under(Layer::Overlay));
let is_overview_open = self.layout.is_overview_open();
// When rendering above the top layer, we put the regular monitor elements first. // When rendering above the top layer, we put the regular monitor elements first.
// Otherwise, we will render all layer-shell pop-ups and the top layer on top. // Otherwise, we will render all layer-shell pop-ups and the top layer on top.
if mon.render_above_top_layer() { if mon.render_above_top_layer() {
@@ -3111,13 +3174,23 @@ impl Niri {
} else { } else {
under = under under = under
.or_else(|| layer_popup_under(Layer::Top)) .or_else(|| layer_popup_under(Layer::Top))
.or_else(|| layer_toplevel_under(Layer::Top)) .or_else(|| layer_toplevel_under(Layer::Top));
.or_else(interactive_moved_window_under)
.or_else(|| layer_popup_under(Layer::Bottom)) under = under.or_else(interactive_moved_window_under);
.or_else(|| layer_popup_under(Layer::Background))
.or_else(window_under) if !is_overview_open {
.or_else(|| layer_toplevel_under(Layer::Bottom)) under = under
.or_else(|| layer_toplevel_under(Layer::Background)); .or_else(|| layer_popup_under(Layer::Bottom))
.or_else(|| layer_popup_under(Layer::Background));
}
under = under.or_else(window_under);
if !is_overview_open {
under = under
.or_else(|| layer_toplevel_under(Layer::Bottom))
.or_else(|| layer_toplevel_under(Layer::Background));
}
} }
let Some((mut surface_and_pos, (window, layer))) = under else { let Some((mut surface_and_pos, (window, layer))) = under else {
@@ -3576,6 +3649,7 @@ impl Niri {
// layer-shell, the layout will briefly draw as active, despite never having focus. // layer-shell, the layout will briefly draw as active, despite never having focus.
KeyboardFocus::LockScreen { .. } => true, KeyboardFocus::LockScreen { .. } => true,
KeyboardFocus::ScreenshotUi => true, KeyboardFocus::ScreenshotUi => true,
KeyboardFocus::Overview => true,
}; };
self.layout.refresh(layout_is_active); self.layout.refresh(layout_is_active);
@@ -3877,7 +3951,7 @@ impl Niri {
// Get monitor elements. // Get monitor elements.
let mon = self.layout.monitor_for_output(output).unwrap(); let mon = self.layout.monitor_for_output(output).unwrap();
let zoom = 1.; let zoom = mon.overview_zoom();
let monitor_elements = Vec::from_iter( let monitor_elements = Vec::from_iter(
mon.render_elements(renderer, target, focus_ring) mon.render_elements(renderer, target, focus_ring)
.map(|(geo, iter)| (geo, Vec::from_iter(iter))), .map(|(geo, iter)| (geo, Vec::from_iter(iter))),
@@ -5560,7 +5634,7 @@ impl Niri {
} }
if let Some(window) = &new_focus.window { if let Some(window) = &new_focus.window {
if current_focus.window.as_ref() != Some(window) { if !self.layout.is_overview_open() && current_focus.window.as_ref() != Some(window) {
let (window, hit) = window; let (window, hit) = window;
// Don't trigger focus-follows-mouse over the tab indicator. // Don't trigger focus-follows-mouse over the tab indicator.
@@ -5809,7 +5883,7 @@ fn scale_relocate_crop<E: Element>(
niri_render_elements! { niri_render_elements! {
OutputRenderElements<R> => { OutputRenderElements<R> => {
Monitor = MonitorRenderElement<R>, Monitor = MonitorRenderElement<R>,
Tile = TileRenderElement<R>, RescaledTile = RescaleRenderElement<TileRenderElement<R>>,
LayerSurface = LayerSurfaceRenderElement<R>, LayerSurface = LayerSurfaceRenderElement<R>,
RelocatedLayerSurface = CropRenderElement<RelocateRenderElement<RescaleRenderElement< RelocatedLayerSurface = CropRenderElement<RelocateRenderElement<RescaleRenderElement<
LayerSurfaceRenderElement<R> LayerSurfaceRenderElement<R>