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)]
pub gestures: Gestures,
#[knuffel(child, default)]
pub overview: Overview,
#[knuffel(child, default)]
pub environment: Environment,
#[knuffel(children(name = "window-rule"))]
pub window_rules: Vec<WindowRule>,
@@ -988,6 +990,8 @@ pub struct Animations {
pub config_notification_open_close: ConfigNotificationOpenCloseAnim,
#[knuffel(child, default)]
pub screenshot_ui_open: ScreenshotUiOpenAnim,
#[knuffel(child, default)]
pub overview_open_close: OverviewOpenCloseAnim,
}
impl Default for Animations {
@@ -1003,6 +1007,7 @@ impl Default for Animations {
window_resize: Default::default(),
config_notification_open_close: 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)]
pub struct Animation {
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)]
pub struct Environment(#[knuffel(children)] pub Vec<EnvironmentVariable>);
@@ -1720,6 +1755,9 @@ pub enum Action {
SetDynamicCastWindowById(u64),
SetDynamicCastMonitor(#[knuffel(argument)] Option<String>),
ClearDynamicCastTarget,
ToggleOverview,
OpenOverview,
CloseOverview,
}
impl From<niri_ipc::Action> for Action {
@@ -1984,6 +2022,9 @@ impl From<niri_ipc::Action> for Action {
Self::SetDynamicCastMonitor(output)
}
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 {
pub fn new_off() -> 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 {
dnd_edge_view_scroll: DndEdgeViewScroll {
@@ -4481,6 +4549,11 @@ mod tests {
),
},
},
overview: Overview {
zoom: FloatOrInt(
0.5,
),
},
environment: Environment(
[
EnvironmentVariable {
+6
View File
@@ -764,6 +764,12 @@ pub enum Action {
},
/// Clear the dynamic cast target, making it show nothing.
ClearDynamicCastTarget {},
/// Toggle (open/close) the Overview.
ToggleOverview {},
/// Open the Overview.
OpenOverview {},
/// Close the Overview.
CloseOverview {},
}
/// Change in window or column size.
+4 -1
View File
@@ -153,7 +153,7 @@ impl XdgShellHandler for State {
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = MoveGrab::new(start_data, window);
let grab = MoveGrab::new(start_data, window, false);
pointer.set_grab(self, grab, serial, Focus::Clear);
}
PointerOrTouchStartData::Touch(start_data) => {
@@ -316,6 +316,9 @@ impl XdgShellHandler for State {
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
// FIXME: somewhere here we probably need to check is_overview_open to match the logic
// in update_keyboard_focus().
if layers
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
.is_none()
+192 -18
View File
@@ -394,7 +394,7 @@ impl State {
}
let bindings = &this.niri.config.borrow().binds;
should_intercept_key(
let res = should_intercept_key(
&mut this.niri.suppressed_keys,
bindings,
mod_key,
@@ -406,7 +406,20 @@ impl State {
&this.niri.screenshot_ui,
this.niri.config.borrow().input.disable_power_key_handling,
is_inhibiting_shortcuts,
)
);
if matches!(res, FilterResult::Forward) {
// If we didn't find any bind, try other hardcoded keys.
if this.niri.keyboard_focus.is_overview() && pressed {
if let Some(bind) = raw.and_then(|raw| hardcoded_overview_bind(raw, *mods))
{
this.niri.suppressed_keys.insert(key_code);
return FilterResult::Intercept(Some(bind));
}
}
}
res
},
) else {
return;
@@ -1915,6 +1928,20 @@ impl State {
Action::ClearDynamicCastTarget => {
self.set_dynamic_cast_target(CastTarget::Nothing);
}
Action::ToggleOverview => {
self.niri.layout.toggle_overview();
self.niri.queue_redraw_all();
}
Action::OpenOverview => {
if self.niri.layout.open_overview() {
self.niri.queue_redraw_all();
}
}
Action::CloseOverview => {
if self.niri.layout.close_overview() {
self.niri.queue_redraw_all();
}
}
}
}
@@ -2235,13 +2262,49 @@ impl State {
self.niri.pointer_hidden = false;
self.niri.tablet_cursor_location = None;
let is_overview_open = self.niri.layout.is_overview_open();
if is_overview_open && !pointer.is_grabbed() && button == Some(MouseButton::Right) {
if let Some((output, ws)) = self.niri.workspace_under_cursor(true) {
let ws_id = ws.id();
let ws_idx = self.niri.layout.find_workspace_by_id(ws_id).unwrap().0;
self.niri.layout.focus_output(&output);
let location = pointer.current_location();
let start_data = PointerGrabStartData {
focus: None,
button: button_code,
location,
};
self.niri
.layout
.view_offset_gesture_begin(&output, Some(ws_idx), false);
let grab = SpatialMovementGrab::new(start_data, output, ws_id, true);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::AllScroll));
// FIXME: granular.
self.niri.queue_redraw_all();
return;
}
}
if button == Some(MouseButton::Middle) && !pointer.is_grabbed() {
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
if mod_down {
let output_ws = self.niri.output_under_cursor().and_then(|output| {
let mon = self.niri.layout.monitor_for_output(&output)?;
Some((output, mon.active_workspace_ref()))
});
let output_ws = if is_overview_open {
self.niri.workspace_under_cursor(true)
} else {
// We don't want to accidentally "catch" the wrong workspace during
// animations.
self.niri.output_under_cursor().and_then(|output| {
let mon = self.niri.layout.monitor_for_output(&output)?;
Some((output, mon.active_workspace_ref()))
})
};
if let Some((output, ws)) = output_ws {
let ws_id = ws.id();
@@ -2254,7 +2317,7 @@ impl State {
button: button_code,
location,
};
let grab = SpatialMovementGrab::new(start_data, output, ws_id);
let grab = SpatialMovementGrab::new(start_data, output, ws_id, false);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri
.cursor_manager
@@ -2276,12 +2339,14 @@ impl State {
// Check if we need to start an interactive move.
if button == Some(MouseButton::Left) && !pointer.is_grabbed() {
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
if mod_down {
if is_overview_open || mod_down {
let location = pointer.current_location();
let (output, pos_within_output) = self.niri.output_under(location).unwrap();
let output = output.clone();
self.niri.layout.activate_window(&window);
if !is_overview_open {
self.niri.layout.activate_window(&window);
}
if self.niri.layout.interactive_move_begin(
window.clone(),
@@ -2293,11 +2358,14 @@ impl State {
button: button_code,
location,
};
let grab = MoveGrab::new(start_data, window.clone());
let grab = MoveGrab::new(start_data, window.clone(), is_overview_open);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
if !is_overview_open {
self.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
}
}
}
}
@@ -2372,7 +2440,20 @@ impl State {
}
}
self.niri.layout.activate_window(&window);
if !is_overview_open {
self.niri.layout.activate_window(&window);
}
// FIXME: granular.
self.niri.queue_redraw_all();
} else if let Some((output, ws)) = is_overview_open
.then(|| self.niri.workspace_under_cursor(false))
.flatten()
{
let ws_idx = self.niri.layout.find_workspace_by_id(ws.id()).unwrap().0;
self.niri.layout.focus_output(&output);
self.niri.layout.toggle_overview_to_workspace(ws_idx);
// FIXME: granular.
self.niri.queue_redraw_all();
@@ -2684,6 +2765,8 @@ impl State {
let tool = self.niri.seat.tablet_seat().get_tool(&event.tool());
if let Some(tool) = tool {
let is_overview_open = self.niri.layout.is_overview_open();
match event.tip_state() {
TabletToolTipState::Down => {
let serial = SERIAL_COUNTER.next_serial();
@@ -2692,8 +2775,31 @@ impl State {
if let Some(pos) = self.niri.tablet_cursor_location {
let under = self.niri.contents_under(pos);
if let Some((window, _)) = under.window {
if let Some(output) = is_overview_open.then_some(under.output).flatten()
{
let mut workspaces = self.niri.layout.workspaces();
if let Some(ws_idx) = workspaces.find_map(|(_, ws_idx, ws)| {
ws.windows().any(|w| w.window == window).then_some(ws_idx)
}) {
drop(workspaces);
self.niri.layout.focus_output(&output);
self.niri.layout.toggle_overview_to_workspace(ws_idx);
}
}
self.niri.layout.activate_window(&window);
// FIXME: granular.
self.niri.queue_redraw_all();
} else if let Some((output, ws)) = is_overview_open
.then(|| self.niri.workspace_under(false, pos))
.flatten()
{
let ws_idx = self.niri.layout.find_workspace_by_id(ws.id()).unwrap().0;
self.niri.layout.focus_output(&output);
self.niri.layout.toggle_overview_to_workspace(ws_idx);
// FIXME: granular.
self.niri.queue_redraw_all();
} else if let Some(output) = under.output {
@@ -2779,6 +2885,12 @@ impl State {
if event.fingers() == 3 {
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.
return;
}
@@ -2816,6 +2928,8 @@ impl State {
delta_y = libinput_event.dy_unaccelerated();
}
let uninverted_delta_y = delta_y;
let device = event.device();
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
if device.config_scroll_natural_scroll_enabled() {
@@ -2824,6 +2938,8 @@ impl State {
}
}
let is_overview_open = self.niri.layout.is_overview_open();
if let Some((cx, cy)) = &mut self.niri.gesture_swipe_3f_cumulative {
*cx += delta_x;
*cy += delta_y;
@@ -2835,10 +2951,16 @@ impl State {
if let Some(output) = self.niri.output_under_cursor() {
if cx.abs() > cy.abs() {
let output_ws = self.niri.output_under_cursor().and_then(|output| {
let mon = self.niri.layout.monitor_for_output(&output)?;
Some((output, mon.active_workspace_ref()))
});
let output_ws = if is_overview_open {
self.niri.workspace_under_cursor(true)
} else {
// We don't want to accidentally "catch" the wrong workspace during
// animations.
self.niri.output_under_cursor().and_then(|output| {
let mon = self.niri.layout.monitor_for_output(&output)?;
Some((output, mon.active_workspace_ref()))
})
};
if let Some((output, ws)) = output_ws {
let ws_idx = self.niri.layout.find_workspace_by_id(ws.id()).unwrap().0;
@@ -2880,6 +3002,17 @@ impl State {
handled = true;
}
let res = self
.niri
.layout
.overview_gesture_update(-uninverted_delta_y, timestamp);
if let Some(redraw) = res {
if redraw {
self.niri.queue_redraw_all();
}
handled = true;
}
if handled {
// We handled this event.
return;
@@ -2916,6 +3049,12 @@ impl State {
handled = true;
}
let res = self.niri.layout.overview_gesture_end();
if res {
self.niri.queue_redraw_all();
handled = true;
}
if handled {
// We handled this event.
return;
@@ -3512,6 +3651,41 @@ fn allowed_during_screenshot(action: &Action) -> bool {
)
}
fn hardcoded_overview_bind(raw: Keysym, mods: ModifiersState) -> Option<Bind> {
let mods = modifiers_from_state(mods);
if !mods.is_empty() {
return None;
}
let mut repeat = true;
let action = match raw {
Keysym::Escape | Keysym::Return => {
repeat = false;
Action::ToggleOverview
}
Keysym::Left => Action::FocusColumnLeft,
Keysym::Right => Action::FocusColumnRight,
Keysym::Up => Action::FocusWindowOrWorkspaceUp,
Keysym::Down => Action::FocusWindowOrWorkspaceDown,
_ => {
return None;
}
};
Some(Bind {
key: Key {
trigger: Trigger::Keysym(raw),
modifiers: Modifiers::empty(),
},
action,
repeat,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: false,
hotkey_overlay_title: None,
})
}
pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::Device) {
// According to Mutter code, this setting is specific to touchpads.
let is_touchpad = device.config_tap_finger_count() > 0;
+42 -5
View File
@@ -1,10 +1,11 @@
use smithay::backend::input::ButtonState;
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
@@ -15,14 +16,32 @@ pub struct MoveGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GestureState {
Recognizing,
Move,
}
impl MoveGrab {
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
pub fn new(
start_data: PointerGrabStartData<State>,
window: Window,
use_threshold: bool,
) -> Self {
let gesture = if use_threshold {
GestureState::Recognizing
} else {
GestureState::Move
};
Self {
last_location: start_data.location,
start_data,
window,
gesture,
}
}
@@ -53,6 +72,24 @@ impl PointerGrab<State> for MoveGrab {
let output = output.clone();
let event_delta = event.location - self.last_location;
self.last_location = event.location;
if self.gesture == GestureState::Recognizing {
let c = event.location - self.start_data.location;
// Check if the gesture moved far enough to decide.
if c.x * c.x + c.y * c.y >= 8. * 8. {
self.gesture = GestureState::Move;
data.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
}
}
if self.gesture != GestureState::Move {
return;
}
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,
+8 -1
View File
@@ -33,13 +33,20 @@ impl SpatialMovementGrab {
start_data: PointerGrabStartData<State>,
output: Output,
workspace_id: WorkspaceId,
is_view_offset: bool,
) -> Self {
let gesture = if is_view_offset {
GestureState::ViewOffset
} else {
GestureState::Recognizing
};
Self {
last_location: start_data.location,
start_data,
output,
workspace_id,
gesture: GestureState::Recognizing,
gesture,
}
}
+394 -44
View File
@@ -45,6 +45,7 @@ use niri_config::{
use niri_ipc::{ColumnDisplay, PositionChange, SizeChange};
use scrolling::{Column, ColumnWidth};
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::utils::RescaleRenderElement;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::output::{self, Output};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@@ -55,7 +56,8 @@ use workspace::{WorkspaceAddWindowTarget, WorkspaceId};
pub use self::monitor::MonitorRenderElement;
use self::monitor::{Monitor, WorkspaceSwitch};
use self::workspace::{OutputId, Workspace};
use crate::animation::Clock;
use crate::animation::{Animation, Clock};
use crate::input::swipe_tracker::SwipeTracker;
use crate::layout::scrolling::ScrollDirection;
use crate::niri_render_elements;
use crate::render_helpers::offscreen::OffscreenData;
@@ -96,6 +98,14 @@ const INTERACTIVE_MOVE_START_THRESHOLD: f64 = 256. * 256.;
/// Opacity of interactively moved tiles targeting the scrolling layout.
const INTERACTIVE_MOVE_ALPHA: f64 = 0.75;
/// Amount of touchpad movement to toggle the overview.
const OVERVIEW_GESTURE_MOVEMENT: f64 = 300.;
const OVERVIEW_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
stiffness: 0.5,
limit: 0.05,
};
/// Size-relative units.
pub struct SizeFrac;
@@ -293,6 +303,13 @@ pub struct Layout<W: LayoutElement> {
clock: Clock,
/// Time that we last updated render elements for.
update_render_elements_time: Duration,
/// Whether the overview is open.
///
/// This is a boolean flag that controls things like where input goes to. The actual animation
/// is controlled by overview_progress.
overview_open: bool,
/// The overview zoom progress.
overview_progress: Option<OverviewProgress>,
/// Configurable properties of the layout.
options: Rc<Options>,
}
@@ -338,6 +355,7 @@ pub struct Options {
pub preset_window_heights: Vec<PresetSize>,
pub animations: niri_config::Animations,
pub gestures: niri_config::Gestures,
pub overview: niri_config::Overview,
// Debug flags.
pub disable_resize_throttling: bool,
pub disable_transactions: bool,
@@ -365,6 +383,7 @@ impl Default for Options {
default_column_width: None,
animations: Default::default(),
gestures: Default::default(),
overview: Default::default(),
disable_resize_throttling: false,
disable_transactions: false,
preset_window_heights: vec![
@@ -493,6 +512,21 @@ pub enum HitType {
},
}
#[derive(Debug)]
enum OverviewProgress {
Animation(Animation),
Gesture(OverviewGesture),
}
#[derive(Debug)]
struct OverviewGesture {
tracker: SwipeTracker,
/// Start point.
start: f64,
/// Current progress.
value: f64,
}
impl<W: LayoutElement> InteractiveMoveState<W> {
fn moving(&self) -> Option<&InteractiveMoveData<W>> {
match self {
@@ -510,16 +544,16 @@ impl<W: LayoutElement> InteractiveMoveState<W> {
}
impl<W: LayoutElement> InteractiveMoveData<W> {
fn tile_render_location(&self) -> Point<f64, Logical> {
fn tile_render_location(&self, zoom: f64) -> Point<f64, Logical> {
let scale = Scale::from(self.output.current_scale().fractional_scale());
let window_size = self.tile.window_size();
let pointer_offset_within_window = Point::from((
window_size.w * self.pointer_ratio_within_window.0,
window_size.h * self.pointer_ratio_within_window.1,
));
let pos =
self.pointer_pos_within_output - pointer_offset_within_window - self.tile.window_loc()
+ self.tile.render_offset();
let pos = self.pointer_pos_within_output
- (pointer_offset_within_window + self.tile.window_loc() - self.tile.render_offset())
.upscale(zoom);
// Round to physical pixels.
pos.to_physical_precise_round(scale).to_logical(scale)
}
@@ -553,6 +587,15 @@ impl HitType {
tile.hit(pos_within_tile)
.map(|hit| (tile.window(), hit.offset_win_pos(tile_pos)))
}
pub fn to_activate(self) -> Self {
match self {
HitType::Input { .. } => HitType::Activate {
is_tab_indicator: false,
},
HitType::Activate { .. } => self,
}
}
}
impl Options {
@@ -594,6 +637,7 @@ impl Options {
default_column_width,
animations: config.animations.clone(),
gestures: config.gestures,
overview: config.overview,
disable_resize_throttling: config.debug.disable_resize_throttling,
disable_transactions: config.debug.disable_transactions,
preset_window_heights,
@@ -611,6 +655,19 @@ impl Options {
}
}
impl OverviewProgress {
fn value(&self) -> f64 {
match self {
OverviewProgress::Animation(anim) => anim.value(),
OverviewProgress::Gesture(gesture) => gesture.value,
}
}
fn is_animation(&self) -> bool {
matches!(self, OverviewProgress::Animation(_))
}
}
impl<W: LayoutElement> Layout<W> {
pub fn new(clock: Clock, config: &Config) -> Self {
Self::with_options_and_workspaces(clock, config, Options::from_config(config))
@@ -625,6 +682,8 @@ impl<W: LayoutElement> Layout<W> {
dnd: None,
clock,
update_render_elements_time: Duration::ZERO,
overview_open: false,
overview_progress: None,
options: Rc::new(options),
}
}
@@ -648,6 +707,8 @@ impl<W: LayoutElement> Layout<W> {
dnd: None,
clock,
update_render_elements_time: Duration::ZERO,
overview_open: false,
overview_progress: None,
options: opts,
}
}
@@ -751,6 +812,8 @@ impl<W: LayoutElement> Layout<W> {
let mut monitor =
Monitor::new(output, workspaces, self.clock.clone(), self.options.clone());
monitor.active_workspace_idx = active_workspace_idx;
monitor.overview_open = self.overview_open;
monitor.set_overview_progress(self.overview_progress.as_ref());
monitors.push(monitor);
MonitorSet::Normal {
@@ -789,6 +852,8 @@ impl<W: LayoutElement> Layout<W> {
let mut monitor =
Monitor::new(output, workspaces, self.clock.clone(), self.options.clone());
monitor.active_workspace_idx = active_workspace_idx;
monitor.overview_open = self.overview_open;
monitor.set_overview_progress(self.overview_progress.as_ref());
MonitorSet::Normal {
monitors: vec![monitor],
@@ -1418,7 +1483,7 @@ impl<W: LayoutElement> Layout<W> {
let mut target = Rectangle::from_size(Size::from((width, height)));
// FIXME: ideally this shouldn't include the tile render offset, but the code
// duplication would be a bit annoying for this edge case.
target.loc.y -= move_.tile_render_location().y;
target.loc.y -= move_.tile_render_location(1.).y;
target.loc.y -= move_.tile.window_loc().y;
return target;
}
@@ -2279,8 +2344,19 @@ impl<W: LayoutElement> Layout<W> {
) -> Option<(&W, HitType)> {
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.output == *output {
let tile_pos = move_.tile_render_location();
HitType::hit_tile(&move_.tile, tile_pos, pos_within_output)
if self.overview_progress.is_some() {
let zoom = self.overview_zoom();
let tile_pos = move_.tile_render_location(zoom);
let pos_within_tile = (pos_within_output - tile_pos).downscale(zoom);
// During the overview animation, we cannot do input hits because we cannot
// really represent scaled windows properly.
let (win, hit) =
HitType::hit_tile(&move_.tile, Point::from((0., 0.)), pos_within_tile)?;
Some((win, hit.to_activate()))
} else {
let tile_pos = move_.tile_render_location(1.);
HitType::hit_tile(&move_.tile, tile_pos, pos_within_output)
}
} else {
None
}
@@ -2316,6 +2392,36 @@ impl<W: LayoutElement> Layout<W> {
mon.resize_edges_under(pos_within_output)
}
pub fn workspace_under(
&self,
extended_bounds: bool,
output: &Output,
pos_within_output: Point<f64, Logical>,
) -> Option<&Workspace<W>> {
if self
.interactive_moved_window_under(output, pos_within_output)
.is_some()
{
return None;
}
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return None;
};
let mon = monitors.iter().find(|mon| &mon.output == output)?;
if extended_bounds {
mon.workspace_under(pos_within_output).map(|(ws, _)| ws)
} else {
mon.workspace_under_narrow(pos_within_output)
}
}
pub fn overview_zoom(&self) -> f64 {
let progress = self.overview_progress.as_ref().map(|p| p.value());
compute_overview_zoom(&self.options, progress)
}
#[cfg(test)]
fn verify_invariants(&self) {
use std::collections::HashSet;
@@ -2324,6 +2430,8 @@ impl<W: LayoutElement> Layout<W> {
use crate::layout::monitor::WorkspaceSwitch;
let zoom = self.overview_zoom();
let mut move_win_id = None;
if let Some(state) = &self.interactive_move {
match state {
@@ -2352,7 +2460,7 @@ impl<W: LayoutElement> Layout<W> {
base options adjusted for output scale"
);
let tile_pos = move_.tile_render_location();
let tile_pos = move_.tile_render_location(zoom);
let rounded_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale);
// Tile position must be rounded to physical pixels.
@@ -2460,6 +2568,12 @@ impl<W: LayoutElement> Layout<W> {
"monitor options must be synchronized with layout"
);
assert_eq!(self.overview_open, monitor.overview_open);
assert_eq!(
self.overview_progress.as_ref().map(|p| p.value()),
monitor.overview_progress_value()
);
if let Some(WorkspaceSwitch::Animation(anim)) = &monitor.workspace_switch {
let before_idx = anim.from() as usize;
let after_idx = anim.to() as usize;
@@ -2650,6 +2764,8 @@ impl<W: LayoutElement> Layout<W> {
// Scroll the view if needed.
if let Some((output, pos_within_output)) = dnd_scroll {
if let Some(mon) = self.monitor_for_output_mut(&output) {
let zoom = mon.overview_zoom();
if let Some((ws, geo)) = mon.workspace_under(pos_within_output) {
let ws_id = ws.id();
let ws = mon
@@ -2657,7 +2773,18 @@ impl<W: LayoutElement> Layout<W> {
.iter_mut()
.find(|ws| ws.id() == ws_id)
.unwrap();
ws.dnd_scroll_gesture_scroll(pos_within_output - geo.loc);
// As far as the DnD scroll gesture is concerned, the workspace spans across
// the whole monitor horizontally.
let ws_pos = Point::from((0., geo.loc.y));
ws.dnd_scroll_gesture_scroll(pos_within_output - ws_pos, 1. / zoom);
}
}
}
if !self.overview_open {
if let Some(OverviewProgress::Animation(anim)) = &mut self.overview_progress {
if anim.is_done() {
self.overview_progress = None;
}
}
}
@@ -2665,6 +2792,7 @@ impl<W: LayoutElement> Layout<W> {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
mon.set_overview_progress(self.overview_progress.as_ref());
mon.advance_animations();
}
}
@@ -2697,6 +2825,14 @@ impl<W: LayoutElement> Layout<W> {
}
}
if self
.overview_progress
.as_ref()
.is_some_and(|p| p.is_animation())
{
return true;
}
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return false;
};
@@ -2719,9 +2855,10 @@ impl<W: LayoutElement> Layout<W> {
self.update_render_elements_time = self.clock.now();
let zoom = self.overview_zoom();
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if output.map_or(true, |output| move_.output == *output) {
let pos_within_output = move_.tile_render_location();
let pos_within_output = move_.tile_render_location(zoom);
let view_rect =
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output));
move_.tile.update_render_elements(true, view_rect);
@@ -2745,6 +2882,7 @@ impl<W: LayoutElement> Layout<W> {
let is_active = self.is_active
&& idx == *active_monitor_idx
&& !matches!(self.interactive_move, Some(InteractiveMoveState::Moving(_)));
mon.set_overview_progress(self.overview_progress.as_ref());
mon.update_render_elements(is_active);
}
}
@@ -2798,6 +2936,7 @@ impl<W: LayoutElement> Layout<W> {
let _span = tracy_client::span!("Layout::update_insert_hint::update");
if let Some(mon) = self.monitor_for_output_mut(&move_.output) {
let zoom = mon.overview_zoom();
if let Some((ws, geo)) = mon.workspace_under(move_.pointer_pos_within_output) {
let ws_id = ws.id();
let ws = mon
@@ -2805,8 +2944,8 @@ impl<W: LayoutElement> Layout<W> {
.iter_mut()
.find(|ws| ws.id() == ws_id)
.unwrap();
let pos_within_workspace = move_.pointer_pos_within_output - geo.loc;
let pos_within_workspace =
(move_.pointer_pos_within_output - geo.loc).downscale(zoom);
let position = ws.scrolling_insert_position(pos_within_workspace);
let rules = move_.tile.window().rules();
@@ -3645,6 +3784,9 @@ impl<W: LayoutElement> Layout<W> {
timestamp: Duration,
is_touchpad: bool,
) -> Option<Option<Output>> {
let zoom = self.overview_zoom();
let delta_x = delta_x / zoom;
let monitors = match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => monitors,
MonitorSet::NoOutputs { .. } => return None,
@@ -3684,6 +3826,77 @@ impl<W: LayoutElement> Layout<W> {
None
}
pub fn overview_gesture_begin(&mut self) {
self.overview_open = true;
let value = self.overview_progress.take().map_or(0., |p| p.value());
let gesture = OverviewGesture {
tracker: SwipeTracker::new(),
start: value,
value,
};
self.overview_progress = Some(OverviewProgress::Gesture(gesture));
self.set_monitors_overview_state();
}
pub fn overview_gesture_update(&mut self, delta_y: f64, timestamp: Duration) -> Option<bool> {
let Some(OverviewProgress::Gesture(gesture)) = &mut self.overview_progress else {
return None;
};
gesture.tracker.push(delta_y, timestamp);
let total_height = OVERVIEW_GESTURE_MOVEMENT;
let pos = gesture.tracker.pos() / total_height;
let new_value = gesture.start + pos;
let new_value = OVERVIEW_GESTURE_RUBBER_BAND.clamp(0., 1., new_value);
if gesture.value == new_value {
return Some(false);
}
gesture.value = new_value;
self.set_monitors_overview_state();
Some(true)
}
pub fn overview_gesture_end(&mut self) -> bool {
let Some(OverviewProgress::Gesture(gesture)) = &mut self.overview_progress else {
return false;
};
// Take into account any idle time between the last event and now.
let now = self.clock.now_unadjusted();
gesture.tracker.push(0., now);
let total_height = OVERVIEW_GESTURE_MOVEMENT;
let mut velocity = gesture.tracker.velocity() / total_height;
let current_pos = gesture.tracker.pos() / total_height;
let pos = gesture.tracker.projected_end_pos() / total_height;
let new_value = gesture.start + pos;
let new_value = new_value.clamp(0., 1.).round();
velocity *=
OVERVIEW_GESTURE_RUBBER_BAND.clamp_derivative(0., 1., gesture.start + current_pos);
self.overview_open = new_value == 1.;
self.overview_progress = Some(OverviewProgress::Animation(Animation::new(
self.clock.clone(),
gesture.value,
new_value,
velocity,
self.options.animations.overview_open_close.0,
)));
self.set_monitors_overview_state();
true
}
pub fn interactive_move_begin(
&mut self,
window_id: W::Id,
@@ -3710,6 +3923,8 @@ impl<W: LayoutElement> Layout<W> {
return false;
}
let zoom = mon.overview_zoom();
let is_floating = ws.is_floating(&window_id);
let (tile, tile_offset, _visible) = ws
.tiles_with_render_positions()
@@ -3717,10 +3932,11 @@ impl<W: LayoutElement> Layout<W> {
.unwrap();
let window_offset = tile.window_loc();
let tile_pos = ws_geo.loc + tile_offset;
let tile_pos = ws_geo.loc + tile_offset.upscale(zoom);
let pointer_offset_within_window = start_pos_within_output - tile_pos - window_offset;
let window_size = tile.window_size();
let pointer_offset_within_window =
start_pos_within_output - tile_pos - window_offset.upscale(zoom);
let window_size = tile.window_size().upscale(zoom);
let pointer_ratio_within_window = (
f64::clamp(pointer_offset_within_window.x / window_size.w, 0., 1.),
f64::clamp(pointer_offset_within_window.y / window_size.h, 0., 1.),
@@ -3768,6 +3984,9 @@ impl<W: LayoutElement> Layout<W> {
return false;
}
let zoom = self.overview_zoom();
let delta = delta.downscale(zoom);
pointer_delta += delta;
let (cx, cy) = (pointer_delta.x, pointer_delta.y);
@@ -3824,7 +4043,8 @@ impl<W: LayoutElement> Layout<W> {
.find(|(tile, _, _)| tile.window().id() == window)
.unwrap();
tile_pos = Some(ws_geo.loc + tile_offset);
let zoom = mon.overview_zoom();
tile_pos = Some((ws_geo.loc + tile_offset.upscale(zoom), zoom));
}
}
}
@@ -3903,9 +4123,10 @@ impl<W: LayoutElement> Layout<W> {
pointer_ratio_within_window,
};
if let Some(tile_pos) = tile_pos {
let new_tile_pos = data.tile_render_location();
data.tile.animate_move_from(tile_pos - new_tile_pos);
if let Some((tile_pos, zoom)) = tile_pos {
let new_tile_pos = data.tile_render_location(zoom);
data.tile
.animate_move_from((tile_pos - new_tile_pos).downscale(zoom));
}
self.interactive_move = Some(InteractiveMoveState::Moving(data));
@@ -3960,12 +4181,16 @@ impl<W: LayoutElement> Layout<W> {
unreachable!()
};
let mut ws_id = None;
for ws in self.workspaces_mut() {
let id = ws.id();
if let Some(tile) = ws.tiles_mut().find(|tile| *tile.window().id() == window_id)
{
let offset = tile.interactive_move_offset;
tile.interactive_move_offset = Point::from((0., 0.));
tile.animate_move_from(offset);
ws_id = Some(id);
}
// Unlock the view on the workspaces, but if the moved window was active,
@@ -3980,6 +4205,32 @@ impl<W: LayoutElement> Layout<W> {
}
}
// In the overview, we want to click on a window to focus it, and also to
// click-and-drag to move the window. The way we handle this is by always starting
// the interactive move (to get frozen view), then, when in the overview, *not*
// calling interactive_move_update() until the cursor moves far enough. This means
// that if we "just click" then we end up in this branch with state == Starting.
// Close the overview in this case.
if self.overview_open {
let ws_id = ws_id.unwrap();
if let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set {
for mon in monitors {
if let Some(ws_idx) =
mon.workspaces.iter().position(|ws| ws.id() == ws_id)
{
mon.activate_workspace_with_anim_config(
ws_idx,
Some(self.options.animations.overview_open_close.0),
);
break;
}
}
}
self.activate_window(&window_id);
self.close_overview();
}
return;
}
InteractiveMoveState::Moving(move_) => move_,
@@ -4007,20 +4258,28 @@ impl<W: LayoutElement> Layout<W> {
);
}
// Dragging in the overview shouldn't switch the workspace and so on.
let activate = if self.overview_open {
ActivateWindow::No
} else {
ActivateWindow::Yes
};
match &mut self.monitor_set {
MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} => {
let (mon, ws_idx, position, offset) = if let Some(mon) =
let (mon, ws_idx, position, offset, zoom) = if let Some(mon) =
monitors.iter_mut().find(|mon| mon.output == move_.output)
{
let zoom = mon.overview_zoom();
let (ws, ws_geo) = mon
.workspace_under(move_.pointer_pos_within_output)
// If the pointer is somehow outside the move output and a workspace switch
// is in progress, this won't necessarily do the expected thing, but also
// that is not really supposed to happen so eh?
// is in progress, this won't necessarily do the expected thing.
.unwrap_or_else(|| mon.workspaces_with_render_geo().next().unwrap());
let ws_id = ws.id();
@@ -4033,14 +4292,16 @@ impl<W: LayoutElement> Layout<W> {
let position = if move_.is_floating {
InsertPosition::Floating
} else {
let pos_within_workspace = move_.pointer_pos_within_output - ws_geo.loc;
let pos_within_workspace =
(move_.pointer_pos_within_output - ws_geo.loc).downscale(zoom);
let ws = &mut mon.workspaces[ws_idx];
ws.scrolling_insert_position(pos_within_workspace)
};
(mon, ws_idx, position, ws_geo.loc)
(mon, ws_idx, position, ws_geo.loc, zoom)
} else {
let mon = &mut monitors[*active_monitor_idx];
let zoom = mon.overview_zoom();
// No point in trying to use the pointer position on the wrong output.
let (ws, ws_geo) = mon.workspaces_with_render_geo().next().unwrap();
@@ -4056,11 +4317,12 @@ impl<W: LayoutElement> Layout<W> {
.iter_mut()
.position(|ws| ws.id() == ws_id)
.unwrap();
(mon, ws_idx, position, ws_geo.loc)
(mon, ws_idx, position, ws_geo.loc, zoom)
};
let win_id = move_.tile.window().id().clone();
let window_render_loc = move_.tile_render_location() + move_.tile.window_loc();
let window_render_loc = move_.tile_render_location(zoom) + move_.tile.window_loc();
match position {
InsertPosition::NewColumn(column_idx) => {
@@ -4071,7 +4333,7 @@ impl<W: LayoutElement> Layout<W> {
id: ws_id,
column_idx: Some(column_idx),
},
ActivateWindow::Yes,
activate,
move_.width,
move_.is_full_width,
false,
@@ -4083,13 +4345,15 @@ impl<W: LayoutElement> Layout<W> {
column_idx,
Some(tile_idx),
move_.tile,
true,
activate == ActivateWindow::Yes,
);
}
InsertPosition::Floating => {
let pos = move_.tile_render_location() - offset;
let tile_render_loc = move_.tile_render_location(zoom);
let mut tile = move_.tile;
let pos = (tile_render_loc - offset).downscale(zoom);
let pos = mon.workspaces[ws_idx].floating_logical_to_size_frac(pos);
tile.floating_pos = Some(pos);
@@ -4106,7 +4370,7 @@ impl<W: LayoutElement> Layout<W> {
id: ws_id,
column_idx: None,
},
ActivateWindow::Yes,
activate,
move_.width,
move_.is_full_width,
true,
@@ -4115,15 +4379,18 @@ impl<W: LayoutElement> Layout<W> {
}
// needed because empty_workspace_above_first could have modified the idx
let ws_idx = mon.active_workspace_idx();
let ws = &mut mon.workspaces[ws_idx];
let (tile, tile_render_loc) = ws
.tiles_with_render_positions_mut(false)
.find(|(tile, _)| tile.window().id() == &win_id)
let (tile, tile_render_loc, ws_geo) = mon
.workspaces_with_render_geo_mut()
.find_map(|(ws, geo)| {
ws.tiles_with_render_positions_mut(false)
.find(|(tile, _)| tile.window().id() == &win_id)
.map(|(tile, tile_render_loc)| (tile, tile_render_loc, geo))
})
.unwrap();
let new_window_render_loc = offset + tile_render_loc + tile.window_loc();
let new_window_render_loc =
ws_geo.loc + (tile_render_loc + tile.window_loc()).upscale(zoom);
tile.animate_move_from(window_render_loc - new_window_render_loc);
tile.animate_move_from((window_render_loc - new_window_render_loc).downscale(zoom));
}
MonitorSet::NoOutputs { workspaces, .. } => {
if workspaces.is_empty() {
@@ -4138,7 +4405,7 @@ impl<W: LayoutElement> Layout<W> {
ws.add_tile(
move_.tile,
WorkspaceAddWindowTarget::Auto,
ActivateWindow::Yes,
activate,
move_.width,
move_.is_full_width,
move_.is_floating,
@@ -4376,6 +4643,60 @@ impl<W: LayoutElement> Layout<W> {
self.unname_workspace_by_id(id);
}
pub fn set_monitors_overview_state(&mut self) {
let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else {
return;
};
for mon in monitors {
mon.overview_open = self.overview_open;
mon.set_overview_progress(self.overview_progress.as_ref());
}
}
pub fn toggle_overview(&mut self) {
self.overview_open = !self.overview_open;
let from = self.overview_progress.take().map_or(0., |p| p.value());
let to = if self.overview_open { 1. } else { 0. };
self.overview_progress = Some(OverviewProgress::Animation(Animation::new(
self.clock.clone(),
from,
to,
0.,
self.options.animations.overview_open_close.0,
)));
self.set_monitors_overview_state();
}
pub fn open_overview(&mut self) -> bool {
if self.overview_open {
return false;
}
self.toggle_overview();
true
}
pub fn close_overview(&mut self) -> bool {
if !self.overview_open {
return false;
}
self.toggle_overview();
true
}
pub fn toggle_overview_to_workspace(&mut self, ws_idx: usize) {
let config = self.options.animations.overview_open_close.0;
if let Some(mon) = self.active_monitor() {
mon.activate_workspace_with_anim_config(ws_idx, Some(config));
}
self.toggle_overview();
}
pub fn start_open_animation_for_window(&mut self, window: &W::Id) {
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if move_.tile.window().id() == window {
@@ -4460,12 +4781,14 @@ impl<W: LayoutElement> Layout<W> {
) {
let _span = tracy_client::span!("Layout::start_close_animation_for_window");
let zoom = self.overview_zoom();
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if move_.tile.window().id() == window {
let Some(snapshot) = move_.tile.take_unmap_snapshot() else {
return;
};
let tile_pos = move_.tile_render_location();
let tile_pos = move_.tile_render_location(zoom);
let tile_size = move_.tile.tile_size();
let output = move_.output.clone();
@@ -4516,7 +4839,7 @@ impl<W: LayoutElement> Layout<W> {
renderer: &mut R,
output: &Output,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
) -> impl Iterator<Item = RescaleRenderElement<TileRenderElement<R>>> + 'a {
if self.update_render_elements_time != self.clock.now() {
error!("clock moved between updating render elements and rendering");
}
@@ -4525,8 +4848,20 @@ impl<W: LayoutElement> Layout<W> {
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if &move_.output == output {
let location = move_.tile_render_location();
rv = Some(move_.tile.render(renderer, location, true, target));
let scale = Scale::from(move_.output.current_scale().fractional_scale());
let zoom = self.overview_zoom();
let location = move_.tile_render_location(zoom);
let iter = move_
.tile
.render(renderer, location, true, target)
.map(move |elem| {
RescaleRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
zoom,
)
});
rv = Some(iter);
}
}
@@ -4589,7 +4924,7 @@ impl<W: LayoutElement> Layout<W> {
}
} else {
// Cancel the view offset gesture after workspace switches, moves, etc.
if ws_idx != mon.active_workspace_idx {
if !self.overview_open && ws_idx != mon.active_workspace_idx {
ws.view_offset_gesture_end(None);
}
}
@@ -4684,6 +5019,10 @@ impl<W: LayoutElement> Layout<W> {
self.windows().any(|(_, win)| win.id() == window)
}
pub fn is_overview_open(&self) -> bool {
self.overview_open
}
fn resolve_scrolling_width(&self, window: &W, width: Option<PresetSize>) -> ColumnWidth {
let width = width.unwrap_or_else(|| PresetSize::Fixed(window.size().w));
match width {
@@ -4709,3 +5048,14 @@ impl<W: LayoutElement> Default for MonitorSet<W> {
Self::NoOutputs { workspaces: vec![] }
}
}
fn compute_overview_zoom(options: &Options, overview_progress: Option<f64>) -> f64 {
// Clamp to some sane values.
let zoom = options.overview.zoom.0.clamp(0.0001, 0.75);
if let Some(p) = overview_progress {
(1. - p * (1. - zoom)).max(0.0001)
} else {
1.
}
}
+216 -20
View File
@@ -5,7 +5,7 @@ use std::time::Duration;
use niri_config::CornerRadius;
use smithay::backend::renderer::element::utils::{
CropRenderElement, Relocate, RelocateRenderElement,
CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Size};
@@ -17,11 +17,12 @@ use super::workspace::{
compute_working_area, OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId,
WorkspaceRenderElement,
};
use super::{ActivateWindow, HitType, LayoutElement, Options};
use super::{compute_overview_zoom, ActivateWindow, HitType, LayoutElement, Options};
use crate::animation::{Animation, Clock};
use crate::input::swipe_tracker::SwipeTracker;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::RenderTarget;
use crate::rubber_band::RubberBand;
use crate::utils::transaction::Transaction;
@@ -68,6 +69,10 @@ pub struct Monitor<W: LayoutElement> {
insert_hint_element: InsertHintElement,
/// Location to render the insert hint element.
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.
pub(super) clock: Clock,
/// Configurable properties of the layout.
@@ -94,6 +99,8 @@ pub struct WorkspaceSwitchGesture {
tracker: SwipeTracker,
/// Whether the gesture is controlled by the touchpad.
is_touchpad: bool,
/// Whether the gesture is clamped to +-1 workspace around the center.
is_clamped: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -116,6 +123,12 @@ struct InsertHintRenderLoc {
location: Point<f64, Logical>,
}
#[derive(Debug)]
pub(super) enum OverviewProgress {
Animation(Animation),
Value(f64),
}
/// Where to put a newly added window.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum MonitorAddWindowTarget<'a, W: LayoutElement> {
@@ -137,10 +150,12 @@ niri_render_elements! {
MonitorInnerRenderElement<R> => {
Workspace = CropRenderElement<WorkspaceRenderElement<R>>,
InsertHint = CropRenderElement<InsertHintRenderElement>,
Shadow = ShadowRenderElement,
}
}
pub type MonitorRenderElement<R> = RelocateRenderElement<MonitorInnerRenderElement<R>>;
pub type MonitorRenderElement<R> =
RelocateRenderElement<RescaleRenderElement<MonitorInnerRenderElement<R>>>;
impl WorkspaceSwitch {
pub fn current_idx(&self) -> f64 {
@@ -183,9 +198,38 @@ impl WorkspaceSwitch {
impl WorkspaceSwitchGesture {
fn min_max(&self, workspace_count: usize) -> (f64, f64) {
let min = self.center_idx.saturating_sub(1) as f64;
let max = (self.center_idx + 1).min(workspace_count - 1) as f64;
(min, max)
if self.is_clamped {
let min = self.center_idx.saturating_sub(1) as f64;
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_element: InsertHintElement::new(options.insert_hint),
insert_hint_render_loc: None,
overview_open: false,
overview_progress: None,
workspace_switch: None,
clock,
options,
@@ -769,7 +815,9 @@ impl<W: LayoutElement> Monitor<W> {
// Make sure the hint is at least partially visible.
if matches!(hint.position, InsertPosition::NewColumn(_)) {
let zoom = self.overview_zoom();
let geo = insert_hint_ws_geo.unwrap();
let geo = geo.downscale(zoom);
area.loc.x = area.loc.x.max(-geo.loc.x - area.size.w / 2.);
area.loc.x = area.loc.x.min(geo.loc.x + geo.size.w - area.size.w / 2.);
@@ -942,6 +990,10 @@ impl<W: LayoutElement> Monitor<W> {
///
/// During animations, assumes the final view position.
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()
}
@@ -962,7 +1014,100 @@ impl<W: LayoutElement> Monitor<W> {
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 {
// 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 {
switch.current_idx()
} else {
@@ -972,19 +1117,24 @@ impl<W: LayoutElement> Monitor<W> {
pub fn workspaces_render_geo(&self) -> impl Iterator<Item = Rectangle<f64, Logical>> {
let scale = self.scale.fractional_scale();
let zoom = 1.;
let zoom = self.overview_zoom();
let ws_size = self.workspace_size(zoom);
let gap = self.workspace_gap(zoom);
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 = round_logical_in_physical(scale, first_ws_y);
// Return position for one-past-last workspace too.
(0..=self.workspaces.len()).map(move |idx| {
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)
})
}
@@ -1026,20 +1176,42 @@ impl<W: LayoutElement> Monitor<W> {
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)> {
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> {
if self.overview_progress.is_some() {
return None;
}
let (ws, geo) = self.workspace_under(pos_within_output)?;
ws.resize_edges_under(pos_within_output - geo.loc)
}
pub fn render_above_top_layer(&self) -> bool {
// Render above the top layer only if the view is stationary.
if self.workspace_switch.is_some() {
if self.workspace_switch.is_some() || self.overview_progress.is_some() {
return false;
}
@@ -1074,7 +1246,7 @@ impl<W: LayoutElement> Monitor<W> {
// rendering for maximized GTK windows.
//
// 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(
Point::from((-i32::MAX / 2, 0)),
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.
let mut insert_hint = None;
if !self.options.insert_hint.off {
@@ -1109,6 +1284,13 @@ impl<W: LayoutElement> Monitor<W> {
let floating = floating.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 iter = insert_hint.take().unwrap().1;
let iter = iter.filter_map(move |elem| {
@@ -1122,9 +1304,10 @@ impl<W: LayoutElement> Monitor<W> {
};
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 elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), zoom);
RelocateRenderElement::from_element(
elem,
// The offset we get from workspaces_with_render_positions() is already
@@ -1149,6 +1332,7 @@ impl<W: LayoutElement> Monitor<W> {
current_idx,
tracker: SwipeTracker::new(),
is_touchpad,
is_clamped: !self.overview_open,
};
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
}
@@ -1167,6 +1351,7 @@ impl<W: LayoutElement> Monitor<W> {
return None;
}
let zoom = self.overview_zoom();
let total_height = if gesture.is_touchpad {
WORKSPACE_GESTURE_MOVEMENT
} else {
@@ -1177,13 +1362,24 @@ impl<W: LayoutElement> Monitor<W> {
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);
let pos = gesture.tracker.pos() / total_height;
let (min, max) = gesture.min_max(self.workspaces.len());
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 {
return Some(false);
@@ -1202,6 +1398,7 @@ impl<W: LayoutElement> Monitor<W> {
return false;
}
let zoom = self.overview_zoom();
let total_height = if gesture.is_touchpad {
WORKSPACE_GESTURE_MOVEMENT
} else {
@@ -1216,6 +1413,9 @@ impl<W: LayoutElement> Monitor<W> {
let now = self.clock.now_unadjusted();
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 current_pos = gesture.tracker.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 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;
velocity *= WORKSPACE_GESTURE_RUBBER_BAND.clamp_derivative(
min,
max,
gesture.start_idx + current_pos,
);
velocity *= rubber_band.clamp_derivative(min, max, gesture.start_idx + current_pos);
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
+20
View File
@@ -603,6 +603,13 @@ enum Op {
WorkspaceSwitchGestureEnd {
is_touchpad: Option<bool>,
},
OverviewGestureBegin,
OverviewGestureUpdate {
#[proptest(strategy = "-400f64..400f64")]
delta: f64,
timestamp: Duration,
},
OverviewGestureEnd,
InteractiveMoveBegin {
#[proptest(strategy = "1..=5usize")]
window: usize,
@@ -658,6 +665,7 @@ enum Op {
#[proptest(strategy = "1..=5usize")]
window: usize,
},
ToggleOverview,
}
impl Op {
@@ -1387,6 +1395,15 @@ impl Op {
Op::WorkspaceSwitchGestureEnd { 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 {
window,
output_idx,
@@ -1440,6 +1457,9 @@ impl Op {
Op::InteractiveResizeEnd { 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::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 smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::{layer_map_for_output, Window};
@@ -17,6 +20,7 @@ use super::floating::{FloatingSpace, FloatingSpaceRenderElement};
use super::scrolling::{
Column, ColumnWidth, ScrollDirection, ScrollingSpace, ScrollingSpaceRenderElement,
};
use super::shadow::Shadow;
use super::tile::{Tile, TileRenderSnapshot};
use super::{
ActivateWindow, HitType, InsertPosition, InteractiveResizeData, LayoutElement, Options,
@@ -25,6 +29,7 @@ use super::{
use crate::animation::Clock;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::RenderTarget;
use crate::utils::id::IdCounter;
use crate::utils::transaction::{Transaction, TransactionBlocker};
@@ -80,6 +85,9 @@ pub struct Workspace<W: LayoutElement> {
/// zones.
working_area: Rectangle<f64, Logical>,
/// This workspace's shadow in the overview.
shadow: Shadow,
/// Clock for driving animations.
pub(super) clock: Clock,
@@ -228,6 +236,17 @@ impl<W: LayoutElement> Workspace<W> {
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 {
scrolling,
floating,
@@ -237,6 +256,7 @@ impl<W: LayoutElement> Workspace<W> {
transform: output.current_transform(),
view_size,
working_area,
shadow: Shadow::new(shadow_config),
output: Some(output),
clock,
base_options,
@@ -281,6 +301,17 @@ impl<W: LayoutElement> Workspace<W> {
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 {
scrolling,
floating,
@@ -291,6 +322,7 @@ impl<W: LayoutElement> Workspace<W> {
original_output,
view_size,
working_area,
shadow: Shadow::new(shadow_config),
clock,
base_options,
options,
@@ -343,6 +375,14 @@ impl<W: LayoutElement> Workspace<W> {
let view_rect = Rectangle::from_size(self.view_size);
self.floating
.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>) {
@@ -370,6 +410,7 @@ impl<W: LayoutElement> Workspace<W> {
pub fn update_shaders(&mut self) {
self.scrolling.update_shaders();
self.floating.update_shaders();
self.shadow.update_shaders();
}
pub fn windows(&self) -> impl Iterator<Item = &W> + '_ {
@@ -1432,6 +1473,13 @@ impl<W: LayoutElement> Workspace<W> {
(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 {
self.scrolling.render_above_top_layer()
}
@@ -1630,7 +1678,7 @@ impl<W: LayoutElement> Workspace<W> {
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 trigger_width = config.trigger_width.0;
@@ -1656,6 +1704,7 @@ impl<W: LayoutElement> Workspace<W> {
// Normalize to [0, 1].
delta / trigger_width
};
let delta = delta * speed;
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::MappedLayer;
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::niri_render_elements;
use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
@@ -470,6 +470,7 @@ pub enum KeyboardFocus {
LayerShell { surface: WlSurface },
LockScreen { surface: Option<WlSurface> },
ScreenshotUi,
Overview,
}
#[derive(Default, Clone, PartialEq)]
@@ -566,6 +567,7 @@ impl KeyboardFocus {
KeyboardFocus::LayerShell { surface } => Some(surface),
KeyboardFocus::LockScreen { surface } => surface.as_ref(),
KeyboardFocus::ScreenshotUi => None,
KeyboardFocus::Overview => None,
}
}
@@ -575,12 +577,17 @@ impl KeyboardFocus {
KeyboardFocus::LayerShell { surface } => Some(surface),
KeyboardFocus::LockScreen { surface } => surface,
KeyboardFocus::ScreenshotUi => None,
KeyboardFocus::Overview => None,
}
}
pub fn is_layout(&self) -> bool {
matches!(self, KeyboardFocus::Layout { .. })
}
pub fn is_overview(&self) -> bool {
matches!(self, KeyboardFocus::Overview)
}
}
pub struct State {
@@ -1026,13 +1033,18 @@ impl State {
let focus_on_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);
// 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
// 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::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));
@@ -1043,6 +1055,11 @@ impl State {
surface = surface.or_else(|| focus_on_layer(Layer::Background));
} else {
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::Background));
surface = surface.or_else(layout_focus);
@@ -1102,7 +1119,9 @@ impl State {
// focused window.
//
// 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 { .. })
&& matches!(focus, KeyboardFocus::Layout { .. })
{
@@ -2910,6 +2929,10 @@ impl Niri {
output: &Output,
pos_within_output: Point<f64, Logical>,
) -> bool {
if self.layout.is_overview_open() {
return false;
}
// Check if some layer-shell surface is on top.
let layers = layer_map_for_output(output);
let layer_popup_under = |layer| {
@@ -2939,6 +2962,42 @@ impl Niri {
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.
///
/// 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 (_, geo) = mon.workspace_under(pos_within_output)?;
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 {
@@ -3096,6 +3157,8 @@ impl Niri {
let mut under =
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.
// Otherwise, we will render all layer-shell pop-ups and the top layer on top.
if mon.render_above_top_layer() {
@@ -3111,13 +3174,23 @@ impl Niri {
} else {
under = under
.or_else(|| layer_popup_under(Layer::Top))
.or_else(|| layer_toplevel_under(Layer::Top))
.or_else(interactive_moved_window_under)
.or_else(|| layer_popup_under(Layer::Bottom))
.or_else(|| layer_popup_under(Layer::Background))
.or_else(window_under)
.or_else(|| layer_toplevel_under(Layer::Bottom))
.or_else(|| layer_toplevel_under(Layer::Background));
.or_else(|| layer_toplevel_under(Layer::Top));
under = under.or_else(interactive_moved_window_under);
if !is_overview_open {
under = under
.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 {
@@ -3576,6 +3649,7 @@ impl Niri {
// layer-shell, the layout will briefly draw as active, despite never having focus.
KeyboardFocus::LockScreen { .. } => true,
KeyboardFocus::ScreenshotUi => true,
KeyboardFocus::Overview => true,
};
self.layout.refresh(layout_is_active);
@@ -3877,7 +3951,7 @@ impl Niri {
// Get monitor elements.
let mon = self.layout.monitor_for_output(output).unwrap();
let zoom = 1.;
let zoom = mon.overview_zoom();
let monitor_elements = Vec::from_iter(
mon.render_elements(renderer, target, focus_ring)
.map(|(geo, iter)| (geo, Vec::from_iter(iter))),
@@ -5560,7 +5634,7 @@ impl Niri {
}
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;
// Don't trigger focus-follows-mouse over the tab indicator.
@@ -5809,7 +5883,7 @@ fn scale_relocate_crop<E: Element>(
niri_render_elements! {
OutputRenderElements<R> => {
Monitor = MonitorRenderElement<R>,
Tile = TileRenderElement<R>,
RescaledTile = RescaleRenderElement<TileRenderElement<R>>,
LayerSurface = LayerSurfaceRenderElement<R>,
RelocatedLayerSurface = CropRenderElement<RelocateRenderElement<RescaleRenderElement<
LayerSurfaceRenderElement<R>