Compare commits

...

7 Commits

Author SHA1 Message Date
Ivan Molodetskikh 9cb2fe4eeb wip extra overview scale 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh cc2549323d wip 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 43c1592ab7 layout/tab_indicator: Use round_max1 where appropriate 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 78fe4a68db layout/monitor: Extract workspaces_render_geo() 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 4fe2722a28 Simplify condition 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 7da5fc6169 Extract is_layout_obscured_under() 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 3d4b762bcc Put the top layer above bottom and background layer popups
Makes it consistent with how window popups are below the top layer, also
will make more sense for the overview.
2025-04-25 11:23:06 +03:00
20 changed files with 2011 additions and 327 deletions
+81 -1
View File
@@ -23,7 +23,8 @@ use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
use smithay::input::keyboard::{Keysym, XkbConfig}; use smithay::input::keyboard::{Keysym, XkbConfig};
use smithay::reexports::input; use smithay::reexports::input;
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.2, 0.2, 0.2, 1.]); pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.25, 0.25, 0.25, 1.]);
pub const DEFAULT_BACKDROP_COLOR: Color = Color::from_array_unpremul([0.15, 0.15, 0.15, 1.]);
pub mod layer_rule; pub mod layer_rule;
@@ -984,6 +985,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 {
@@ -999,6 +1002,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(),
} }
} }
} }
@@ -1146,6 +1150,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,
@@ -1183,6 +1203,8 @@ pub struct SpringParams {
pub struct Gestures { pub struct Gestures {
#[knuffel(child, default)] #[knuffel(child, default)]
pub dnd_edge_view_scroll: DndEdgeViewScroll, pub dnd_edge_view_scroll: DndEdgeViewScroll,
#[knuffel(child, default)]
pub dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch,
} }
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
@@ -1205,6 +1227,26 @@ impl Default for DndEdgeViewScroll {
} }
} }
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct DndEdgeWorkspaceSwitch {
#[knuffel(child, unwrap(argument), default = Self::default().trigger_height)]
pub trigger_height: FloatOrInt<0, 65535>,
#[knuffel(child, unwrap(argument), default = Self::default().delay_ms)]
pub delay_ms: u16,
#[knuffel(child, unwrap(argument), default = Self::default().max_speed)]
pub max_speed: FloatOrInt<0, 1_000_000>,
}
impl Default for DndEdgeWorkspaceSwitch {
fn default() -> Self {
Self {
trigger_height: FloatOrInt(50.),
delay_ms: 100,
max_speed: FloatOrInt(1500.),
}
}
}
#[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>);
@@ -1716,6 +1758,7 @@ pub enum Action {
SetDynamicCastWindowById(u64), SetDynamicCastWindowById(u64),
SetDynamicCastMonitor(#[knuffel(argument)] Option<String>), SetDynamicCastMonitor(#[knuffel(argument)] Option<String>),
ClearDynamicCastTarget, ClearDynamicCastTarget,
ToggleOverview,
} }
impl From<niri_ipc::Action> for Action { impl From<niri_ipc::Action> for Action {
@@ -1980,6 +2023,7 @@ 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,
} }
} }
} }
@@ -2964,6 +3008,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 {
@@ -4459,6 +4518,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 {
@@ -4470,6 +4541,15 @@ mod tests {
50.0, 50.0,
), ),
}, },
dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch {
trigger_height: FloatOrInt(
50.0,
),
delay_ms: 100,
max_speed: FloatOrInt(
1500.0,
),
},
}, },
environment: Environment( environment: Environment(
[ [
+2
View File
@@ -764,6 +764,8 @@ pub enum Action {
}, },
/// Clear the dynamic cast target, making it show nothing. /// Clear the dynamic cast target, making it show nothing.
ClearDynamicCastTarget {}, ClearDynamicCastTarget {},
/// Toggle the Overview.
ToggleOverview {},
} }
/// Change in window or column size. /// Change in window or column size.
+1
View File
@@ -266,6 +266,7 @@ impl TestCase for Layout {
.monitor_for_output(&self.output) .monitor_for_output(&self.output)
.unwrap() .unwrap()
.render_elements(renderer, RenderTarget::Output, true) .render_elements(renderer, RenderTarget::Output, true)
.flat_map(|(_, iter)| iter)
.map(|elem| Box::new(elem) as _) .map(|elem| Box::new(elem) as _)
.collect() .collect()
} }
+1
View File
@@ -115,6 +115,7 @@ impl TestCase for Tile {
self.tile.update_render_elements( self.tile.update_render_elements(
true, true,
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)), Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
1.,
); );
self.tile self.tile
.render(renderer, location, true, RenderTarget::Output) .render(renderer, location, true, RenderTarget::Output)
+17 -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()
@@ -1062,6 +1065,19 @@ impl State {
// The target geometry for the positioner should be relative to its parent's geometry, so // The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here. // we will compute that here.
let mut target = Rectangle::from_size(output_geo.size); let mut target = Rectangle::from_size(output_geo.size);
// Background and bottom layer popups render below the top and the overlay layer, so let's
// put them into the non-exclusive zone.
//
// FIXME: ideally this should use the "top and overlay layer" non-exclusive zone, but
// Smithay only computes the "all layers" non-exclusive zone atm.
//
// FIXME: related to the above, top layer popups should use the "overlay layer"
// non-exclusive zone.
if matches!(layer_surface.layer(), Layer::Background | Layer::Bottom) {
target = map.non_exclusive_zone();
}
target.loc -= layer_geo.loc; target.loc -= layer_geo.loc;
target.loc -= get_popup_toplevel_coords(popup); target.loc -= get_popup_toplevel_coords(popup);
+241 -26
View File
@@ -29,7 +29,7 @@ use smithay::input::touch::{
}; };
use smithay::input::SeatHandler; use smithay::input::SeatHandler;
use smithay::output::Output; use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Transform, SERIAL_COUNTER}; use smithay::utils::{Logical, Point, Rectangle, Size, Transform, SERIAL_COUNTER};
use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitor; use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitor;
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint}; use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint};
use smithay::wayland::selection::data_device::DnDGrab; use smithay::wayland::selection::data_device::DnDGrab;
@@ -393,6 +393,15 @@ impl State {
return FilterResult::Intercept(None); return FilterResult::Intercept(None);
} }
if this.niri.keyboard_focus.is_overview()
&& pressed
&& matches!(raw, Some(Keysym::Escape | Keysym::Return))
{
this.niri.layout.toggle_overview();
this.niri.suppressed_keys.insert(key_code);
return FilterResult::Intercept(None);
}
let bindings = &this.niri.config.borrow().binds; let bindings = &this.niri.config.borrow().binds;
should_intercept_key( should_intercept_key(
&mut this.niri.suppressed_keys, &mut this.niri.suppressed_keys,
@@ -1915,10 +1924,18 @@ 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();
}
} }
} }
fn on_pointer_motion<I: InputBackend>(&mut self, event: I::PointerMotionEvent) { fn on_pointer_motion<I: InputBackend>(&mut self, event: I::PointerMotionEvent) {
let was_inside_hot_corner = self.niri.pointer_inside_hot_corner;
// Any of the early returns here mean that the pointer is not inside the hot corner.
self.niri.pointer_inside_hot_corner = false;
// We need an output to be able to move the pointer. // We need an output to be able to move the pointer.
if self.niri.global_space.outputs().next().is_none() { if self.niri.global_space.outputs().next().is_none() {
return; return;
@@ -2095,6 +2112,18 @@ impl State {
pointer.frame(self); pointer.frame(self);
// contents_under() will return no surface when the hot corner should trigger.
if pointer.current_focus().is_none() {
let hot_corner = Rectangle::from_size(Size::from((1., 1.)));
if let Some((_, pos_within_output)) = self.niri.output_under(pos) {
let inside_hot_corner = hot_corner.contains(pos_within_output);
if inside_hot_corner && !was_inside_hot_corner {
self.niri.layout.toggle_overview();
}
self.niri.pointer_inside_hot_corner = inside_hot_corner;
}
}
// Activate a new confinement if necessary. // Activate a new confinement if necessary.
self.niri.maybe_activate_pointer_constraint(); self.niri.maybe_activate_pointer_constraint();
@@ -2119,6 +2148,10 @@ impl State {
&mut self, &mut self,
event: I::PointerMotionAbsoluteEvent, event: I::PointerMotionAbsoluteEvent,
) { ) {
let was_inside_hot_corner = self.niri.pointer_inside_hot_corner;
// Any of the early returns here mean that the pointer is not inside the hot corner.
self.niri.pointer_inside_hot_corner = false;
let Some(pos) = self.compute_absolute_location(&event, None).or_else(|| { let Some(pos) = self.compute_absolute_location(&event, None).or_else(|| {
self.global_bounding_rectangle().map(|output_geo| { self.global_bounding_rectangle().map(|output_geo| {
event.position_transformed(output_geo.size) + output_geo.loc.to_f64() event.position_transformed(output_geo.size) + output_geo.loc.to_f64()
@@ -2164,6 +2197,18 @@ impl State {
pointer.frame(self); pointer.frame(self);
// contents_under() will return no surface when the hot corner should trigger.
if pointer.current_focus().is_none() {
let hot_corner = Rectangle::from_size(Size::from((1., 1.)));
if let Some((_, pos_within_output)) = self.niri.output_under(pos) {
let inside_hot_corner = hot_corner.contains(pos_within_output);
if inside_hot_corner && !was_inside_hot_corner {
self.niri.layout.toggle_overview();
}
self.niri.pointer_inside_hot_corner = inside_hot_corner;
}
}
self.niri.maybe_activate_pointer_constraint(); self.niri.maybe_activate_pointer_constraint();
// We moved the pointer, show it. // We moved the pointer, show it.
@@ -2235,10 +2280,54 @@ 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();
// Don't activate the window under the cursor to avoid unnecessary
// scrolling when e.g. Mod+MMB clicking on a partially off-screen window.
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 {
if let Some(output) = self.niri.output_under_cursor() { let output_ws = if is_overview_open {
self.niri.workspace_under_cursor(true)
} else {
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();
self.niri.layout.focus_output(&output); self.niri.layout.focus_output(&output);
let location = pointer.current_location(); let location = pointer.current_location();
@@ -2247,7 +2336,7 @@ impl State {
button: button_code, button: button_code,
location, location,
}; };
let grab = SpatialMovementGrab::new(start_data, output); 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
@@ -2269,12 +2358,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(),
@@ -2286,11 +2377,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));
}
} }
} }
} }
@@ -2365,7 +2459,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();
@@ -2437,23 +2544,63 @@ impl State {
let horizontal_amount_v120 = event.amount_v120(Axis::Horizontal); let horizontal_amount_v120 = event.amount_v120(Axis::Horizontal);
let vertical_amount_v120 = event.amount_v120(Axis::Vertical); let vertical_amount_v120 = event.amount_v120(Axis::Vertical);
let is_overview_open = self.niri.layout.is_overview_open();
// Handle wheel scroll bindings. // Handle wheel scroll bindings.
if source == AxisSource::Wheel { if source == AxisSource::Wheel {
// If we have a scroll bind with current modifiers, then accumulate and don't pass to // If we have a scroll bind with current modifiers, then accumulate and don't pass to
// Wayland. If there's no bind, reset the accumulator. // Wayland. If there's no bind, reset the accumulator.
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state(); let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let modifiers = modifiers_from_state(mods); let modifiers = modifiers_from_state(mods);
if self.niri.mods_with_wheel_binds.contains(&modifiers) { let should_handle =
is_overview_open || self.niri.mods_with_wheel_binds.contains(&modifiers);
if should_handle {
let horizontal = horizontal_amount_v120.unwrap_or(0.); let horizontal = horizontal_amount_v120.unwrap_or(0.);
let ticks = self.niri.horizontal_wheel_tracker.accumulate(horizontal); let ticks = self.niri.horizontal_wheel_tracker.accumulate(horizontal);
if ticks != 0 { if ticks != 0 {
let config = self.niri.config.borrow(); let (bind_left, bind_right) = if is_overview_open {
let bindings = &config.binds; if modifiers.is_empty() {
let bind_left = let bind_left = Some(Bind {
find_configured_bind(bindings, mod_key, Trigger::WheelScrollLeft, mods); key: Key {
let bind_right = trigger: Trigger::WheelScrollLeft,
find_configured_bind(bindings, mod_key, Trigger::WheelScrollRight, mods); modifiers: Modifiers::empty(),
drop(config); },
action: Action::FocusColumnLeft,
repeat: true,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: false,
hotkey_overlay_title: None,
});
let bind_right = Some(Bind {
key: Key {
trigger: Trigger::WheelScrollRight,
modifiers: Modifiers::empty(),
},
action: Action::FocusColumnRight,
repeat: true,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: false,
hotkey_overlay_title: None,
});
(bind_left, bind_right)
} else {
(None, None)
}
} else {
let config = self.niri.config.borrow();
let bindings = &config.binds;
let bind_left =
find_configured_bind(bindings, mod_key, Trigger::WheelScrollLeft, mods);
let bind_right = find_configured_bind(
bindings,
mod_key,
Trigger::WheelScrollRight,
mods,
);
(bind_left, bind_right)
};
if let Some(right) = bind_right { if let Some(right) = bind_right {
for _ in 0..ticks { for _ in 0..ticks {
@@ -2470,13 +2617,45 @@ impl State {
let vertical = vertical_amount_v120.unwrap_or(0.); let vertical = vertical_amount_v120.unwrap_or(0.);
let ticks = self.niri.vertical_wheel_tracker.accumulate(vertical); let ticks = self.niri.vertical_wheel_tracker.accumulate(vertical);
if ticks != 0 { if ticks != 0 {
let config = self.niri.config.borrow(); let (bind_up, bind_down) = if is_overview_open {
let bindings = &config.binds; if modifiers.is_empty() {
let bind_up = let bind_up = Some(Bind {
find_configured_bind(bindings, mod_key, Trigger::WheelScrollUp, mods); key: Key {
let bind_down = trigger: Trigger::WheelScrollUp,
find_configured_bind(bindings, mod_key, Trigger::WheelScrollDown, mods); modifiers: Modifiers::empty(),
drop(config); },
action: Action::FocusWorkspaceUp,
repeat: true,
cooldown: Some(Duration::from_millis(50)),
allow_when_locked: false,
allow_inhibiting: false,
hotkey_overlay_title: None,
});
let bind_down = Some(Bind {
key: Key {
trigger: Trigger::WheelScrollDown,
modifiers: Modifiers::empty(),
},
action: Action::FocusWorkspaceDown,
repeat: true,
cooldown: Some(Duration::from_millis(50)),
allow_when_locked: false,
allow_inhibiting: false,
hotkey_overlay_title: None,
});
(bind_up, bind_down)
} else {
(None, None)
}
} else {
let config = self.niri.config.borrow();
let bindings = &config.binds;
let bind_up =
find_configured_bind(bindings, mod_key, Trigger::WheelScrollUp, mods);
let bind_down =
find_configured_bind(bindings, mod_key, Trigger::WheelScrollDown, mods);
(bind_up, bind_down)
};
if let Some(down) = bind_down { if let Some(down) = bind_down {
for _ in 0..ticks { for _ in 0..ticks {
@@ -2771,6 +2950,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 +3001,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;
@@ -2827,7 +3014,21 @@ 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() {
self.niri.layout.view_offset_gesture_begin(&output, true); let output_ws = if is_overview_open {
self.niri.workspace_under_cursor(true)
} else {
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;
self.niri
.layout
.view_offset_gesture_begin(&output, Some(ws_idx), true);
}
} else { } else {
self.niri self.niri
.layout .layout
@@ -2862,6 +3063,14 @@ impl State {
handled = true; handled = true;
} }
let res = self.niri.layout.overview_gesture_update(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;
@@ -2904,6 +3113,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;
+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,
+26 -4
View File
@@ -10,12 +10,14 @@ use smithay::input::SeatHandler;
use smithay::output::Output; use smithay::output::Output;
use smithay::utils::{Logical, Point}; use smithay::utils::{Logical, Point};
use crate::layout::workspace::WorkspaceId;
use crate::niri::State; use crate::niri::State;
pub struct SpatialMovementGrab { pub struct SpatialMovementGrab {
start_data: PointerGrabStartData<State>, start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>, last_location: Point<f64, Logical>,
output: Output, output: Output,
workspace_id: WorkspaceId,
gesture: GestureState, gesture: GestureState,
} }
@@ -27,12 +29,24 @@ enum GestureState {
} }
impl SpatialMovementGrab { impl SpatialMovementGrab {
pub fn new(start_data: PointerGrabStartData<State>, output: Output) -> Self { pub fn new(
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 { Self {
last_location: start_data.location, last_location: start_data.location,
start_data, start_data,
output, output,
gesture: GestureState::Recognizing, workspace_id,
gesture,
} }
} }
@@ -81,8 +95,16 @@ impl PointerGrab<State> for SpatialMovementGrab {
if c.x * c.x + c.y * c.y >= 8. * 8. { if c.x * c.x + c.y * c.y >= 8. * 8. {
if c.x.abs() > c.y.abs() { if c.x.abs() > c.y.abs() {
self.gesture = GestureState::ViewOffset; self.gesture = GestureState::ViewOffset;
layout.view_offset_gesture_begin(&self.output, false); if let Some((ws_idx, ws)) = layout.find_workspace_by_id(self.workspace_id) {
layout.view_offset_gesture_update(-c.x, timestamp, false) if ws.current_output() == Some(&self.output) {
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
layout.view_offset_gesture_update(-c.x, timestamp, false)
} else {
None
}
} else {
None
}
} else { } else {
self.gesture = GestureState::WorkspaceSwitch; self.gesture = GestureState::WorkspaceSwitch;
layout.workspace_switch_gesture_begin(&self.output, false); layout.workspace_switch_gesture_begin(&self.output, false);
+7 -2
View File
@@ -263,7 +263,12 @@ impl<W: LayoutElement> FloatingSpace<W> {
self.tiles.iter().any(Tile::are_transitions_ongoing) || !self.closing_windows.is_empty() self.tiles.iter().any(Tile::are_transitions_ongoing) || !self.closing_windows.is_empty()
} }
pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) { pub fn update_render_elements(
&mut self,
is_active: bool,
view_rect: Rectangle<f64, Logical>,
extra_scale: f64,
) {
let active = self.active_window_id.clone(); let active = self.active_window_id.clone();
for (tile, offset) in self.tiles_with_offsets_mut() { for (tile, offset) in self.tiles_with_offsets_mut() {
let id = tile.window().id(); let id = tile.window().id();
@@ -271,7 +276,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
let mut tile_view_rect = view_rect; let mut tile_view_rect = view_rect;
tile_view_rect.loc -= offset + tile.render_offset(); tile_view_rect.loc -= offset + tile.render_offset();
tile.update_render_elements(is_active, tile_view_rect); tile.update_render_elements(is_active, tile_view_rect, extra_scale);
} }
} }
+4 -1
View File
@@ -9,6 +9,7 @@ use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement; use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::utils::round_logical_in_physical_max1;
#[derive(Debug)] #[derive(Debug)]
pub struct FocusRing { pub struct FocusRing {
@@ -64,7 +65,9 @@ impl FocusRing {
scale: f64, scale: f64,
alpha: f32, alpha: f32,
) { ) {
let width = self.config.width.0; // let scale = scale * extra_visual_scale;
// let width = self.config.width.0 / extra_visual_scale;
let width = round_logical_in_physical_max1(scale, self.config.width.0);
self.full_size = win_size + Size::from((width, width)).upscale(2.); self.full_size = win_size + Size::from((width, width)).upscale(2.);
let color = if is_active { let color = if is_active {
+573 -65
View File
File diff suppressed because it is too large Load Diff
+541 -128
View File
@@ -1,9 +1,10 @@
use std::cmp::min; use std::cmp::min;
use std::iter::zip;
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
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};
@@ -13,10 +14,12 @@ use super::tile::Tile;
use super::workspace::{ use super::workspace::{
OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId, WorkspaceRenderElement, OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId, WorkspaceRenderElement,
}; };
use super::{ActivateWindow, HitType, LayoutElement, Options}; use super::{ActivateWindow, HitType, LayoutElement, Options, OVERVIEW_WORKSPACE_SCALE};
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::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;
@@ -30,6 +33,11 @@ const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
limit: 0.05, limit: 0.05,
}; };
/// Amount of DnD edge scrolling to scroll the height of one workspace.
///
/// This constant is tied to the default dnd-edge-workspace-switch max-speed setting.
const WORKSPACE_DND_EDGE_SCROLL_MOVEMENT: f64 = 1500.;
#[derive(Debug)] #[derive(Debug)]
pub struct Monitor<W: LayoutElement> { pub struct Monitor<W: LayoutElement> {
/// Output for this monitor. /// Output for this monitor.
@@ -44,6 +52,10 @@ pub struct Monitor<W: LayoutElement> {
pub(super) previous_workspace_id: Option<WorkspaceId>, pub(super) previous_workspace_id: Option<WorkspaceId>,
/// In-progress switch between workspaces. /// In-progress switch between workspaces.
pub(super) workspace_switch: Option<WorkspaceSwitch>, pub(super) workspace_switch: Option<WorkspaceSwitch>,
/// 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.
@@ -65,6 +77,22 @@ 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,
// If this gesture is for drag-and-drop scrolling, this is the last event's unadjusted
// timestamp.
dnd_last_event_time: Option<Duration>,
// Time when the drag-and-drop scroll delta became non-zero, used for debouncing.
//
// If `None` then the scroll delta is currently zero.
dnd_nonzero_start_time: Option<Duration>,
}
#[derive(Debug)]
pub(super) enum OverviewProgress {
Animation(Animation),
Value(f64),
} }
/// Where to put a newly added window. /// Where to put a newly added window.
@@ -84,8 +112,13 @@ pub enum MonitorAddWindowTarget<'a, W: LayoutElement> {
NextTo(&'a W::Id), NextTo(&'a W::Id),
} }
pub type MonitorRenderElement<R> = niri_render_elements! {
RelocateRenderElement<CropRenderElement<WorkspaceRenderElement<R>>>; MonitorRenderElement<R> => {
Workspace = RelocateRenderElement<RescaleRenderElement<CropRenderElement<
WorkspaceRenderElement<R>>>>,
Shadow = RelocateRenderElement<RescaleRenderElement<ShadowRenderElement>>,
}
}
impl WorkspaceSwitch { impl WorkspaceSwitch {
pub fn current_idx(&self) -> f64 { pub fn current_idx(&self) -> f64 {
@@ -125,6 +158,31 @@ impl WorkspaceSwitch {
} }
} }
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),
}
}
}
impl<W: LayoutElement> Monitor<W> { impl<W: LayoutElement> Monitor<W> {
pub fn new( pub fn new(
output: Output, output: Output,
@@ -138,6 +196,8 @@ impl<W: LayoutElement> Monitor<W> {
workspaces, workspaces,
active_workspace_idx: 0, active_workspace_idx: 0,
previous_workspace_id: None, previous_workspace_id: None,
overview_open: false,
overview_progress: None,
workspace_switch: None, workspace_switch: None,
clock, clock,
options, options,
@@ -211,17 +271,21 @@ impl<W: LayoutElement> Monitor<W> {
self.workspaces.push(ws); self.workspaces.push(ws);
} }
fn activate_workspace(&mut self, idx: usize) { pub fn activate_workspace(&mut self, idx: usize) {
self.activate_workspace_with_anim_config(idx, None);
}
pub fn activate_workspace_with_anim_config(
&mut self,
idx: usize,
config: Option<niri_config::Animation>,
) {
if self.active_workspace_idx == idx { if self.active_workspace_idx == idx {
return; return;
} }
// FIXME: also compute and use current velocity. // FIXME: also compute and use current velocity.
let current_idx = self let current_idx = self.workspace_render_idx();
.workspace_switch
.as_ref()
.map(|s| s.current_idx())
.unwrap_or(self.active_workspace_idx as f64);
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id()); self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
@@ -232,7 +296,7 @@ impl<W: LayoutElement> Monitor<W> {
current_idx, current_idx,
idx as f64, idx as f64,
0., 0.,
self.options.animations.workspace_switch.0, config.unwrap_or(self.options.animations.workspace_switch.0),
))); )));
} }
@@ -638,11 +702,32 @@ impl<W: LayoutElement> Monitor<W> {
} }
pub fn advance_animations(&mut self) { pub fn advance_animations(&mut self) {
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch { match &mut self.workspace_switch {
if anim.is_done() { Some(WorkspaceSwitch::Animation(anim)) => {
self.workspace_switch = None; if anim.is_done() {
self.clean_up_workspaces(); self.workspace_switch = None;
self.clean_up_workspaces();
}
} }
Some(WorkspaceSwitch::Gesture(gesture)) => {
// Make sure the last event time doesn't go too much out of date (for
// monitors not under cursor), causing sudden jumps.
//
// This happens after any dnd_scroll_gesture_scroll() calls (in
// Layout::advance_animations()), so it doesn't mess up the time delta there.
if let Some(last_time) = &mut gesture.dnd_last_event_time {
let now = self.clock.now_unadjusted();
if *last_time != now {
*last_time = now;
// If last_time was already == now, then dnd_scroll_gesture_scroll() must've
// updated the gesture already. Therefore, when this code runs, the pointer
// must be outside the DnD scrolling zone.
gesture.dnd_nonzero_start_time = None;
}
}
}
None => (),
} }
for ws in &mut self.workspaces { for ws in &mut self.workspaces {
@@ -666,31 +751,10 @@ impl<W: LayoutElement> Monitor<W> {
} }
pub fn update_render_elements(&mut self, is_active: bool) { pub fn update_render_elements(&mut self, is_active: bool) {
match &self.workspace_switch { let is_overview_open = self.overview_open;
Some(switch) => { let extra_overview_scale = self.workspace_scale();
let render_idx = switch.current_idx(); for (ws, _) in self.workspaces_with_render_geo_mut() {
let before_idx = render_idx.floor(); ws.update_render_elements(is_active, is_overview_open, extra_overview_scale);
let after_idx = render_idx.ceil();
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return;
}
let after_idx = after_idx as usize;
if after_idx < self.workspaces.len() {
self.workspaces[after_idx].update_render_elements(is_active);
if before_idx < 0. {
return;
}
}
let before_idx = before_idx as usize;
self.workspaces[before_idx].update_render_elements(is_active);
}
None => {
self.workspaces[self.active_workspace_idx].update_render_elements(is_active);
}
} }
} }
@@ -826,6 +890,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;
}
// TODO: unify logic
let mut rect = self.active_workspace_ref().active_tile_visual_rectangle()?; let mut rect = self.active_workspace_ref().active_tile_visual_rectangle()?;
if let Some(switch) = &self.workspace_switch { if let Some(switch) = &self.workspace_switch {
@@ -841,77 +909,221 @@ impl<W: LayoutElement> Monitor<W> {
Some(rect) Some(rect)
} }
pub fn workspaces_with_render_positions( pub fn workspace_scale(&self) -> f64 {
&self, if let Some(p) = &self.overview_progress {
) -> impl Iterator<Item = (&Workspace<W>, Point<f64, Logical>)> { (1. - p.value() * (1. - OVERVIEW_WORKSPACE_SCALE)).max(0.)
let mut first = None; } else {
let mut second = None; 1.
}
}
match &self.workspace_switch { pub(super) fn set_overview_progress(&mut self, progress: Option<&super::OverviewProgress>) {
Some(switch) => { let prev_render_idx = self.workspace_render_idx();
let render_idx = switch.current_idx(); self.overview_progress = progress.map(OverviewProgress::from);
let before_idx = render_idx.floor(); let new_render_idx = self.workspace_render_idx();
let after_idx = render_idx.ceil();
if after_idx >= 0. && before_idx < self.workspaces.len() as f64 { // If the view jumped (can happen when going from corrected to uncorrected render_idx, for
let scale = self.output.current_scale().fractional_scale(); // example when toggling the overview in the middle of an overview animation), then restart
let size = output_size(&self.output); // the workspace switch to avoid jumps.
let offset = if prev_render_idx != new_render_idx {
round_logical_in_physical(scale, (render_idx - before_idx) * size.h); if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
// FIXME: maintain velocity.
// Ceil the height in physical pixels. *anim = anim.restarted(prev_render_idx, anim.to(), 0.);
let height = (size.h * scale).ceil() / scale;
if before_idx >= 0. {
let before_idx = before_idx as usize;
let before_offset = Point::from((0., -offset));
first = Some((&self.workspaces[before_idx], before_offset));
}
let after_idx = after_idx as usize;
if after_idx < self.workspaces.len() {
let after_offset = Point::from((0., -offset + height));
second = Some((&self.workspaces[after_idx], after_offset));
}
}
}
None => {
first = Some((
&self.workspaces[self.active_workspace_idx],
Point::from((0., 0.)),
));
} }
} }
}
first.into_iter().chain(second) #[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
{
let scale = self.output.current_scale().fractional_scale();
let size = output_size(&self.output);
#[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_ws_scale.
//
// - Current height:
// current_height = (size.h + gap) * ws_scale.
//
// - 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_ws_scale = (1. - from * (1. - OVERVIEW_WORKSPACE_SCALE)).max(0.);
let from_ws_height = round_logical_in_physical(scale, size.h * from_ws_scale);
let from_gap = round_logical_in_physical(scale, size.h * from_ws_scale * 0.1);
let from_ws_height_gap = from_ws_height + from_gap;
let ws_scale = self.workspace_scale();
let ws_height = round_logical_in_physical(scale, size.h * ws_scale);
let gap = round_logical_in_physical(scale, size.h * ws_scale * 0.1);
let ws_height_gap = ws_height + gap;
let first_ws_y = -switch_anim.value() * from_ws_height_gap
+ switch_anim.to() * (from_ws_height_gap - ws_height_gap);
return -first_ws_y / ws_height_gap;
}
};
if let Some(switch) = &self.workspace_switch {
switch.current_idx()
} else {
self.active_workspace_idx as f64
}
}
pub fn workspaces_render_geo(&self) -> impl Iterator<Item = Rectangle<f64, Logical>> {
let scale = self.output.current_scale().fractional_scale();
let size = output_size(&self.output);
let ws_scale = self.workspace_scale();
let ws_size = size
.upscale(ws_scale)
.to_physical_precise_ceil(scale)
.to_logical(scale);
let gap = round_logical_in_physical(scale, size.h * 0.1 * ws_scale);
let ws_height_gap = ws_size.h + gap;
let static_offset = (size.to_point() - ws_size.to_point()).downscale(2.);
// let static_offset = Point::from((0., 0.));
// Compute the offset in such a way that if render_idx is active_workspace_idx and the
// workspace scale is 1., then its offset will be (0., 0.).
let first_ws_y = -self.workspace_render_idx() * ws_height_gap;
(0..self.workspaces.len()).map(move |idx| {
let y = first_ws_y + idx as f64 * ws_height_gap;
let loc = Point::from((0., y)) + static_offset;
let loc = loc.to_physical_precise_round(scale).to_logical(scale);
Rectangle::new(loc, ws_size)
})
}
pub fn workspaces_with_render_geo(
&self,
) -> impl Iterator<Item = (&Workspace<W>, Rectangle<f64, Logical>)> {
let output_size = output_size(&self.output);
let output_geo = Rectangle::new(Point::from((0., 0.)), output_size);
let geo = self.workspaces_render_geo();
zip(self.workspaces.iter(), geo)
// Cull out workspaces outside the output.
.filter(move |(_ws, geo)| geo.intersection(output_geo).is_some())
}
pub fn workspaces_with_render_geo_mut(
&mut self,
) -> impl Iterator<Item = (&mut Workspace<W>, Rectangle<f64, Logical>)> {
let output_size = output_size(&self.output);
let output_geo = Rectangle::new(Point::from((0., 0.)), output_size);
let geo = self.workspaces_render_geo();
zip(self.workspaces.iter_mut(), geo)
// Cull out workspaces outside the output.
.filter(move |(_ws, geo)| geo.intersection(output_geo).is_some())
} }
pub fn workspace_under( pub fn workspace_under(
&self, &self,
pos_within_output: Point<f64, Logical>, pos_within_output: Point<f64, Logical>,
) -> Option<(&Workspace<W>, Point<f64, Logical>)> { ) -> Option<(&Workspace<W>, Rectangle<f64, Logical>)> {
let size = output_size(&self.output); let size = output_size(&self.output);
let (ws, bounds) = self let (ws, geo) = self.workspaces_with_render_geo().find_map(|(ws, geo)| {
.workspaces_with_render_positions() // Extend width to entire output.
.map(|(ws, offset)| (ws, Rectangle::new(offset, size))) let loc = Point::from((0., geo.loc.y));
.find(|(_, bounds)| bounds.contains(pos_within_output))?; let size = Size::from((size.w, geo.size.h));
Some((ws, bounds.loc)) let bounds = Rectangle::new(loc, size);
bounds.contains(pos_within_output).then_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, offset) = self.workspace_under(pos_within_output)?; let (ws, geo) = self.workspace_under(pos_within_output)?;
let (win, hit) = ws.window_under(pos_within_output - offset)?;
Some((win, hit.offset_win_pos(offset))) if self.overview_progress.is_some() {
let ws_scale = self.workspace_scale().max(0.0001);
let pos_within_workspace = (pos_within_output - geo.loc).downscale(ws_scale);
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> {
let (ws, offset) = self.workspace_under(pos_within_output)?; if self.overview_progress.is_some() {
ws.resize_edges_under(pos_within_output - offset) 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 { 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;
} }
@@ -924,7 +1136,12 @@ impl<W: LayoutElement> Monitor<W> {
renderer: &'a mut R, renderer: &'a mut R,
target: RenderTarget, target: RenderTarget,
focus_ring: bool, focus_ring: bool,
) -> impl Iterator<Item = MonitorRenderElement<R>> + 'a { ) -> impl Iterator<
Item = (
Rectangle<f64, Logical>,
impl Iterator<Item = MonitorRenderElement<R>>,
),
> + 'a {
let _span = tracy_client::span!("Monitor::render_elements"); let _span = tracy_client::span!("Monitor::render_elements");
let scale = self.output.current_scale().fractional_scale(); let scale = self.output.current_scale().fractional_scale();
@@ -942,7 +1159,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)),
@@ -954,38 +1171,102 @@ impl<W: LayoutElement> Monitor<W> {
) )
}; };
self.workspaces_with_render_positions() let ws_scale = self.workspace_scale();
.flat_map(move |(ws, offset)| { let overview_clamped_progress = self.overview_progress.as_ref().map(|p| p.clamped_value());
ws.render_elements(renderer, target, focus_ring) let is_overview_open = self.overview_open;
.filter_map(move |elem| { self.workspaces_with_render_geo().map(move |(ws, geo)| {
CropRenderElement::from_element(elem, scale, crop_bounds) let iter = ws
}) .render_elements(renderer, target, focus_ring, is_overview_open)
.map(move |elem| { .filter_map(move |elem| CropRenderElement::from_element(elem, scale, crop_bounds))
RelocateRenderElement::from_element( // .map(move |elem| {
elem, // let elem_scale = 1. - (1. - ws_scale) / OVERVIEW_WORKSPACE_SCALE * 0.03;
// The offset we get from workspaces_with_render_positions() is already // RescaleRenderElement::from_element(
// rounded to physical pixels, but it's in the logical coordinate // elem,
// space, so we need to convert it to physical. // size.downscale(2.)
offset.to_physical_precise_round(scale), // .to_physical_precise_round(scale)
Relocate::Relative, // .to_point(),
) // elem_scale,
}) // )
}) // })
.map(move |elem| {
RescaleRenderElement::from_element(elem, Point::from((0, 0)), ws_scale)
})
.map(move |elem| {
RelocateRenderElement::from_element(
elem,
// The offset we get from workspaces_with_render_positions() is already
// rounded to physical pixels, but it's in the logical coordinate
// space, so we need to convert it to physical.
geo.loc.to_physical_precise_round(scale),
Relocate::Relative,
)
})
.map(MonitorRenderElement::Workspace);
let shadow = if let Some(value) = overview_clamped_progress {
Vec::from_iter(
ws.render_shadow(renderer)
.map(move |elem| elem.with_alpha(value.clamp(0., 1.) as f32))
.map(move |elem| {
RescaleRenderElement::from_element(elem, Point::from((0, 0)), ws_scale)
})
.map(move |elem| {
RelocateRenderElement::from_element(
elem,
geo.loc.to_physical_precise_round(scale),
Relocate::Relative,
)
})
.map(MonitorRenderElement::Shadow),
)
} else {
Vec::new()
};
(geo, iter.chain(shadow))
})
} }
pub fn workspace_switch_gesture_begin(&mut self, is_touchpad: bool) { pub fn workspace_switch_gesture_begin(&mut self, is_touchpad: bool) {
let center_idx = self.active_workspace_idx; let center_idx = self.active_workspace_idx;
let current_idx = self let current_idx = self.workspace_render_idx();
.workspace_switch
.as_ref()
.map(|s| s.current_idx())
.unwrap_or(center_idx as f64);
let gesture = WorkspaceSwitchGesture { let gesture = WorkspaceSwitchGesture {
center_idx, center_idx,
current_idx, current_idx,
tracker: SwipeTracker::new(), tracker: SwipeTracker::new(),
is_touchpad, is_touchpad,
is_clamped: !self.overview_open,
dnd_last_event_time: None,
dnd_nonzero_start_time: None,
};
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
}
pub fn dnd_scroll_gesture_begin(&mut self) {
if let Some(WorkspaceSwitch::Gesture(WorkspaceSwitchGesture {
dnd_last_event_time: Some(_),
..
})) = &self.workspace_switch
{
// Already active.
return;
}
if !self.overview_open {
// This gesture is only for the overview.
return;
}
let center_idx = self.active_workspace_idx;
let current_idx = self.workspace_render_idx();
let gesture = WorkspaceSwitchGesture {
center_idx,
current_idx,
tracker: SwipeTracker::new(),
is_touchpad: false,
is_clamped: false,
dnd_last_event_time: Some(self.clock.now_unadjusted()),
dnd_nonzero_start_time: None,
}; };
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture)); self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
} }
@@ -996,27 +1277,46 @@ impl<W: LayoutElement> Monitor<W> {
timestamp: Duration, timestamp: Duration,
is_touchpad: bool, is_touchpad: bool,
) -> Option<bool> { ) -> Option<bool> {
let ws_scale = self.workspace_scale().max(0.0001);
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else { let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return None; return None;
}; };
if gesture.is_touchpad != is_touchpad { if gesture.is_touchpad != is_touchpad || gesture.dnd_last_event_time.is_some() {
return None; return None;
} }
// Reduce the effect of ws_scale on the touchpad somewhat.
let delta_scale = if gesture.is_touchpad {
(ws_scale - 1.) / 2.5 + 1.
} else {
ws_scale
};
let delta_y = delta_y / delta_scale;
let mut rubber_band = WORKSPACE_GESTURE_RUBBER_BAND;
rubber_band.limit /= ws_scale;
gesture.tracker.push(delta_y, timestamp); gesture.tracker.push(delta_y, timestamp);
let total_height = if gesture.is_touchpad { let total_height = if gesture.is_touchpad {
WORKSPACE_GESTURE_MOVEMENT WORKSPACE_GESTURE_MOVEMENT
} else { } else {
self.workspaces[0].view_size().h // Account for the gap.
self.workspaces[0].view_size().h * 1.1
}; };
let pos = gesture.tracker.pos() / total_height; let pos = gesture.tracker.pos() / total_height;
let min = gesture.center_idx.saturating_sub(1) as f64; let (min, max) = if gesture.is_clamped {
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64; let min = gesture.center_idx.saturating_sub(1) as f64;
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
(min, max)
} else {
(0., (self.workspaces.len() - 1) as f64)
};
let new_idx = gesture.center_idx as f64 + pos; let new_idx = gesture.center_idx as f64 + 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);
@@ -1026,11 +1326,102 @@ impl<W: LayoutElement> Monitor<W> {
Some(true) Some(true)
} }
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>, speed: f64) -> bool {
let ws_scale = self.workspace_scale();
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return false;
};
let Some(last_time) = gesture.dnd_last_event_time else {
// Not a DnD scroll.
return false;
};
let config = &self.options.gestures.dnd_edge_workspace_switch;
let trigger_height = config.trigger_height.0;
// This working area intentionally does not include extra struts from Options.
// TODO: working area
let output_size = output_size(&self.output);
let width = output_size.w * ws_scale;
let x = pos.x - (output_size.w - width) / 2.;
let y = pos.y;
let height = output_size.h;
let y = y.clamp(0., height);
let trigger_height = trigger_height.clamp(0., height / 2.);
let delta = if x < 0. || width <= x {
// Outside the bounds horizontally.
0.
} else if y < trigger_height {
-(trigger_height - y)
} else if height - y < trigger_height {
trigger_height - (height - y)
} else {
0.
};
let delta = if trigger_height < 0.01 {
// Sanity check for trigger-height 0 or small window sizes.
0.
} else {
// Normalize to [0, 1].
delta / trigger_height
};
let delta = delta * speed;
let now = self.clock.now_unadjusted();
gesture.dnd_last_event_time = Some(now);
if delta == 0. {
// We're outside the scrolling zone.
gesture.dnd_nonzero_start_time = None;
return false;
}
let nonzero_start = *gesture.dnd_nonzero_start_time.get_or_insert(now);
// Delay starting the gesture a bit to avoid unwanted movement when dragging across
// monitors.
let delay = Duration::from_millis(u64::from(config.delay_ms));
if now.saturating_sub(nonzero_start) < delay {
return true;
}
let time_delta = now.saturating_sub(last_time).as_secs_f64();
let delta = delta * time_delta * config.max_speed.0;
gesture.tracker.push(delta, now);
let total_height = WORKSPACE_DND_EDGE_SCROLL_MOVEMENT;
let pos = gesture.tracker.pos() / total_height;
let (min, max) = if gesture.is_clamped {
let min = gesture.center_idx.saturating_sub(1) as f64;
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
(min, max)
} else {
(0., (self.workspaces.len() - 1) as f64)
};
let new_idx = gesture.center_idx as f64 + pos;
let new_idx = new_idx.clamp(min, max);
gesture.current_idx = new_idx;
true
}
pub fn workspace_switch_gesture_end( pub fn workspace_switch_gesture_end(
&mut self, &mut self,
cancelled: bool, cancelled: bool,
is_touchpad: Option<bool>, is_touchpad: Option<bool>,
) -> bool { ) -> bool {
let ws_scale = self.workspace_scale().max(0.0001);
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else { let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return false; return false;
}; };
@@ -1049,28 +1440,35 @@ 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 /= ws_scale;
let total_height = if gesture.is_touchpad { let total_height = if gesture.is_touchpad {
WORKSPACE_GESTURE_MOVEMENT WORKSPACE_GESTURE_MOVEMENT
} else if gesture.dnd_last_event_time.is_some() {
WORKSPACE_DND_EDGE_SCROLL_MOVEMENT
} else { } else {
self.workspaces[0].view_size().h // Account for the gap.
self.workspaces[0].view_size().h * 1.1
}; };
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;
let min = gesture.center_idx.saturating_sub(1) as f64; let (min, max) = if gesture.is_clamped {
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64; let min = gesture.center_idx.saturating_sub(1) as f64;
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
(min, max)
} else {
(0., (self.workspaces.len() - 1) as f64)
};
let new_idx = gesture.center_idx as f64 + pos; let new_idx = gesture.center_idx as f64 + 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.center_idx as f64 + current_pos);
min,
max,
gesture.center_idx as f64 + 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());
@@ -1085,4 +1483,19 @@ impl<W: LayoutElement> Monitor<W> {
true true
} }
pub fn dnd_scroll_gesture_end(&mut self) {
if !matches!(
self.workspace_switch,
Some(WorkspaceSwitch::Gesture(WorkspaceSwitchGesture {
dnd_last_event_time: Some(_),
..
}))
) {
// Not a DnD scroll.
return;
};
self.workspace_switch_gesture_end(false, None);
}
} }
+77 -21
View File
@@ -21,7 +21,7 @@ use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget; use crate::render_helpers::RenderTarget;
use crate::utils::transaction::{Transaction, TransactionBlocker}; use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::ResizeEdge; use crate::utils::{round_logical_in_physical_max1, ResizeEdge};
use crate::window::ResolvedWindowRules; use crate::window::ResolvedWindowRules;
/// Amount of touchpad movement to scroll the view for the width of one working area. /// Amount of touchpad movement to scroll the view for the width of one working area.
@@ -84,6 +84,11 @@ pub struct ScrollingSpace<W: LayoutElement> {
/// Scale of the output the space is on (and rounds its sizes to). /// Scale of the output the space is on (and rounds its sizes to).
scale: f64, scale: f64,
/// Extra scale used for rendering.
///
/// Applied on top of `scale` and used for visuals only (does not affect the layout).
extra_overview_scale: f64,
/// Clock for driving animations. /// Clock for driving animations.
clock: Clock, clock: Clock,
@@ -288,6 +293,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
view_size, view_size,
working_area, working_area,
scale, scale,
extra_overview_scale: 1.,
clock, clock,
options, options,
} }
@@ -371,7 +377,14 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|| !self.closing_windows.is_empty() || !self.closing_windows.is_empty()
} }
pub fn update_render_elements(&mut self, is_active: bool) { pub fn update_render_elements(
&mut self,
is_active: bool,
is_overview_open: bool,
extra_overview_scale: f64,
) {
self.extra_overview_scale = extra_overview_scale;
let view_pos = Point::from((self.view_pos(), 0.)); let view_pos = Point::from((self.view_pos(), 0.));
let view_size = self.view_size; let view_size = self.view_size;
let active_idx = self.active_column_idx; let active_idx = self.active_column_idx;
@@ -380,11 +393,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
let col_off = Point::from((col_x, 0.)); let col_off = Point::from((col_x, 0.));
let col_pos = view_pos - col_off - col.render_offset(); let col_pos = view_pos - col_off - col.render_offset();
let view_rect = Rectangle::new(col_pos, view_size); let view_rect = Rectangle::new(col_pos, view_size);
col.update_render_elements(is_active, view_rect); col.update_render_elements(is_active, view_rect, extra_overview_scale);
} }
if let Some(insert_hint) = &self.insert_hint { if let Some(insert_hint) = &self.insert_hint {
if let Some(area) = self.insert_hint_area(insert_hint) { if let Some(area) = self.insert_hint_area(insert_hint, !is_overview_open) {
let view_rect = Rectangle::new(area.loc.upscale(-1.), view_size); let view_rect = Rectangle::new(area.loc.upscale(-1.), view_size);
self.insert_hint_element.update_render_elements( self.insert_hint_element.update_render_elements(
area.size, area.size,
@@ -2183,14 +2196,41 @@ impl<W: LayoutElement> ScrollingSpace<W> {
.unwrap() .unwrap()
} }
fn visual_column_xs(
&self,
data: impl Iterator<Item = ColumnData>,
) -> impl Iterator<Item = f64> {
let visual_scale = self.scale * self.extra_overview_scale;
let gaps = round_logical_in_physical_max1(visual_scale, self.options.gaps);
// let gaps = self.options.gaps / self.extra_overview_scale;
let mut x = 0.;
// Chain with a dummy value to be able to get one past all columns' X.
let dummy = ColumnData { width: 0. };
let data = data.chain(iter::once(dummy));
data.map(move |data| {
let rv = x;
let width = round_logical_in_physical_max1(visual_scale, data.width);
// let width = data.width;
x += width + gaps;
rv
})
}
fn column_xs_in_render_order( fn column_xs_in_render_order(
&self, &self,
data: impl Iterator<Item = ColumnData>, data: impl Iterator<Item = ColumnData>,
) -> impl Iterator<Item = f64> { ) -> impl Iterator<Item = f64> {
let active_idx = self.active_column_idx; let active_idx = self.active_column_idx;
let active_pos = self.column_x(active_idx); // let active_pos = self.column_x(active_idx);
let active_pos = self
.visual_column_xs(self.data.iter().copied())
.nth(active_idx)
.unwrap();
let offsets = self let offsets = self
.column_xs(data) .visual_column_xs(data)
.enumerate() .enumerate()
.filter_map(move |(idx, pos)| (idx != active_idx).then_some(pos)); .filter_map(move |(idx, pos)| (idx != active_idx).then_some(pos));
iter::once(active_pos).chain(offsets) iter::once(active_pos).chain(offsets)
@@ -2234,7 +2274,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
pub fn tiles_with_render_positions( pub fn tiles_with_render_positions(
&self, &self,
) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>, bool)> { ) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>, bool)> {
let scale = self.scale; let scale = self.scale * self.extra_overview_scale;
let view_off = Point::from((-self.view_pos(), 0.)); let view_off = Point::from((-self.view_pos(), 0.));
self.columns_in_render_order() self.columns_in_render_order()
.flat_map(move |(col, col_x)| { .flat_map(move |(col, col_x)| {
@@ -2255,7 +2295,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
&mut self, &mut self,
round: bool, round: bool,
) -> impl Iterator<Item = (&mut Tile<W>, Point<f64, Logical>)> { ) -> impl Iterator<Item = (&mut Tile<W>, Point<f64, Logical>)> {
let scale = self.scale; let scale = self.scale * self.extra_overview_scale;
let view_off = Point::from((-self.view_pos(), 0.)); let view_off = Point::from((-self.view_pos(), 0.));
self.columns_in_render_order_mut() self.columns_in_render_order_mut()
.flat_map(move |(col, col_x)| { .flat_map(move |(col, col_x)| {
@@ -2274,7 +2314,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
}) })
} }
fn insert_hint_area(&self, insert_hint: &InsertHint) -> Option<Rectangle<f64, Logical>> { fn insert_hint_area(
&self,
insert_hint: &InsertHint,
clamp_to_view: bool,
) -> Option<Rectangle<f64, Logical>> {
let mut hint_area = match insert_hint.position { let mut hint_area = match insert_hint.position {
InsertPosition::NewColumn(column_index) => { InsertPosition::NewColumn(column_index) => {
if column_index == 0 || column_index == self.columns.len() { if column_index == 0 || column_index == self.columns.len() {
@@ -2369,7 +2413,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
let view_size = self.view_size; let view_size = self.view_size;
// Make sure the hint is at least partially visible. // Make sure the hint is at least partially visible.
if matches!(insert_hint.position, InsertPosition::NewColumn(_)) { if clamp_to_view && matches!(insert_hint.position, InsertPosition::NewColumn(_)) {
hint_area.loc.x = hint_area.loc.x.max(-hint_area.size.w / 2.); hint_area.loc.x = hint_area.loc.x.max(-hint_area.size.w / 2.);
hint_area.loc.x = hint_area.loc.x.min(view_size.w - hint_area.size.w / 2.); hint_area.loc.x = hint_area.loc.x.min(view_size.w - hint_area.size.w / 2.);
} }
@@ -2724,14 +2768,16 @@ impl<W: LayoutElement> ScrollingSpace<W> {
renderer: &mut R, renderer: &mut R,
target: RenderTarget, target: RenderTarget,
focus_ring: bool, focus_ring: bool,
is_overview_open: bool,
) -> Vec<ScrollingSpaceRenderElement<R>> { ) -> Vec<ScrollingSpaceRenderElement<R>> {
let mut rv = vec![]; let mut rv = vec![];
let scale = Scale::from(self.scale); let scale = Scale::from(self.scale);
let visual_scale = scale * self.extra_overview_scale;
// Draw the insert hint. // Draw the insert hint.
if let Some(insert_hint) = &self.insert_hint { if let Some(insert_hint) = &self.insert_hint {
if let Some(area) = self.insert_hint_area(insert_hint) { if let Some(area) = self.insert_hint_area(insert_hint, !is_overview_open) {
rv.extend( rv.extend(
self.insert_hint_element self.insert_hint_element
.render(renderer, area.loc) .render(renderer, area.loc)
@@ -2762,7 +2808,9 @@ impl<W: LayoutElement> ScrollingSpace<W> {
// Draw the tab indicator on top. // Draw the tab indicator on top.
{ {
let pos = view_off + col_off + col_render_off; let pos = view_off + col_off + col_render_off;
let pos = pos.to_physical_precise_round(scale).to_logical(scale); let pos = pos
.to_physical_precise_round(visual_scale)
.to_logical(visual_scale);
rv.extend(col.tab_indicator.render(renderer, pos).map(Into::into)); rv.extend(col.tab_indicator.render(renderer, pos).map(Into::into));
} }
@@ -2770,7 +2818,9 @@ impl<W: LayoutElement> ScrollingSpace<W> {
let tile_pos = let tile_pos =
view_off + col_off + col_render_off + tile_off + tile.render_offset(); view_off + col_off + col_render_off + tile_off + tile.render_offset();
// Round to physical pixels. // Round to physical pixels.
let tile_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale); let tile_pos = tile_pos
.to_physical_precise_round(visual_scale)
.to_logical(visual_scale);
// And now the drawing logic. // And now the drawing logic.
@@ -2916,14 +2966,14 @@ impl<W: LayoutElement> ScrollingSpace<W> {
Some(true) Some(true)
} }
pub fn dnd_scroll_gesture_scroll(&mut self, delta: f64) { pub fn dnd_scroll_gesture_scroll(&mut self, delta: f64) -> bool {
let ViewOffset::Gesture(gesture) = &mut self.view_offset else { let ViewOffset::Gesture(gesture) = &mut self.view_offset else {
return; return false;
}; };
let Some(last_time) = gesture.dnd_last_event_time else { let Some(last_time) = gesture.dnd_last_event_time else {
// Not a DnD scroll. // Not a DnD scroll.
return; return false;
}; };
let config = &self.options.gestures.dnd_edge_view_scroll; let config = &self.options.gestures.dnd_edge_view_scroll;
@@ -2934,7 +2984,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
if delta == 0. { if delta == 0. {
// We're outside the scrolling zone. // We're outside the scrolling zone.
gesture.dnd_nonzero_start_time = None; gesture.dnd_nonzero_start_time = None;
return; return false;
} }
let nonzero_start = *gesture.dnd_nonzero_start_time.get_or_insert(now); let nonzero_start = *gesture.dnd_nonzero_start_time.get_or_insert(now);
@@ -2943,7 +2993,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
// monitors. // monitors.
let delay = Duration::from_millis(u64::from(config.delay_ms)); let delay = Duration::from_millis(u64::from(config.delay_ms));
if now.saturating_sub(nonzero_start) < delay { if now.saturating_sub(nonzero_start) < delay {
return; return true;
} }
let time_delta = now.saturating_sub(last_time).as_secs_f64(); let time_delta = now.saturating_sub(last_time).as_secs_f64();
@@ -2987,6 +3037,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
gesture.delta_from_tracker += clamped_offset - view_offset; gesture.delta_from_tracker += clamped_offset - view_offset;
gesture.current_view_offset = clamped_offset; gesture.current_view_offset = clamped_offset;
true
} }
pub fn view_offset_gesture_end(&mut self, _cancelled: bool, is_touchpad: Option<bool>) -> bool { pub fn view_offset_gesture_end(&mut self, _cancelled: bool, is_touchpad: Option<bool>) -> bool {
@@ -3816,14 +3867,19 @@ impl<W: LayoutElement> Column<W> {
|| self.tiles.iter().any(Tile::are_transitions_ongoing) || self.tiles.iter().any(Tile::are_transitions_ongoing)
} }
pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) { pub fn update_render_elements(
&mut self,
is_active: bool,
view_rect: Rectangle<f64, Logical>,
extra_overview_scale: f64,
) {
let active_idx = self.active_tile_idx; let active_idx = self.active_tile_idx;
for (tile_idx, (tile, tile_off)) in self.tiles_mut().enumerate() { for (tile_idx, (tile, tile_off)) in self.tiles_mut().enumerate() {
let is_active = is_active && tile_idx == active_idx; let is_active = is_active && tile_idx == active_idx;
let mut tile_view_rect = view_rect; let mut tile_view_rect = view_rect;
tile_view_rect.loc -= tile_off + tile.render_offset(); tile_view_rect.loc -= tile_off + tile.render_offset();
tile.update_render_elements(is_active, tile_view_rect); tile.update_render_elements(is_active, tile_view_rect, extra_overview_scale);
} }
let config = self.tab_indicator.config(); let config = self.tab_indicator.config();
@@ -3849,7 +3905,7 @@ impl<W: LayoutElement> Column<W> {
self.tiles.len(), self.tiles.len(),
tabs, tabs,
is_active, is_active,
self.scale, self.scale * extra_overview_scale,
); );
} }
+7 -4
View File
@@ -10,7 +10,9 @@ use crate::animation::{Animation, Clock};
use crate::niri_render_elements; use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement; use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::renderer::NiriRenderer;
use crate::utils::{floor_logical_in_physical_max1, round_logical_in_physical}; use crate::utils::{
floor_logical_in_physical_max1, round_logical_in_physical, round_logical_in_physical_max1,
};
#[derive(Debug)] #[derive(Debug)]
pub struct TabIndicator { pub struct TabIndicator {
@@ -77,12 +79,13 @@ impl TabIndicator {
scale: f64, scale: f64,
) -> impl Iterator<Item = Rectangle<f64, Logical>> { ) -> impl Iterator<Item = Rectangle<f64, Logical>> {
let round = |logical: f64| round_logical_in_physical(scale, logical); let round = |logical: f64| round_logical_in_physical(scale, logical);
let round_max1 = |logical: f64| round_logical_in_physical_max1(scale, logical);
let progress = self.open_anim.as_ref().map_or(1., |a| a.value().max(0.)); let progress = self.open_anim.as_ref().map_or(1., |a| a.value().max(0.));
let width = round(self.config.width.0); let width = round_max1(self.config.width.0);
let gap = round(self.config.gap.0); let gap = round_max1(self.config.gap.0);
let gaps_between = round(self.config.gaps_between_tabs.0); let gaps_between = round_max1(self.config.gaps_between_tabs.0);
let position = self.config.position; let position = self.config.position;
let side = match position { let side = match position {
+25 -1
View File
@@ -576,6 +576,8 @@ enum Op {
ViewOffsetGestureBegin { ViewOffsetGestureBegin {
#[proptest(strategy = "1..=5usize")] #[proptest(strategy = "1..=5usize")]
output_idx: usize, output_idx: usize,
#[proptest(strategy = "proptest::option::of(0..=4usize)")]
workspace_idx: Option<usize>,
is_touchpad: bool, is_touchpad: bool,
}, },
ViewOffsetGestureUpdate { ViewOffsetGestureUpdate {
@@ -602,6 +604,13 @@ enum Op {
cancelled: bool, cancelled: bool,
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,
@@ -657,6 +666,7 @@ enum Op {
#[proptest(strategy = "1..=5usize")] #[proptest(strategy = "1..=5usize")]
window: usize, window: usize,
}, },
ToggleOverview,
} }
impl Op { impl Op {
@@ -1345,6 +1355,7 @@ impl Op {
} }
Op::ViewOffsetGestureBegin { Op::ViewOffsetGestureBegin {
output_idx: id, output_idx: id,
workspace_idx,
is_touchpad: normalize, is_touchpad: normalize,
} => { } => {
let name = format!("output{id}"); let name = format!("output{id}");
@@ -1352,7 +1363,7 @@ impl Op {
return; return;
}; };
layout.view_offset_gesture_begin(&output, normalize); layout.view_offset_gesture_begin(&output, workspace_idx, normalize);
} }
Op::ViewOffsetGestureUpdate { Op::ViewOffsetGestureUpdate {
delta, delta,
@@ -1389,6 +1400,15 @@ impl Op {
} => { } => {
layout.workspace_switch_gesture_end(cancelled, is_touchpad); layout.workspace_switch_gesture_end(cancelled, 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,
@@ -1442,6 +1462,9 @@ impl Op {
Op::InteractiveResizeEnd { window } => { Op::InteractiveResizeEnd { window } => {
layout.interactive_resize_end(&window); layout.interactive_resize_end(&window);
} }
Op::ToggleOverview => {
layout.toggle_overview();
}
} }
} }
} }
@@ -2265,6 +2288,7 @@ fn unfullscreen_view_offset_not_reset_on_gesture() {
Op::FullscreenWindow(1), Op::FullscreenWindow(1),
Op::ViewOffsetGestureBegin { Op::ViewOffsetGestureBegin {
output_idx: 1, output_idx: 1,
workspace_idx: None,
is_touchpad: true, is_touchpad: true,
}, },
Op::ViewOffsetGestureEnd { Op::ViewOffsetGestureEnd {
+73 -11
View File
@@ -25,8 +25,8 @@ use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::snapshot::RenderSnapshot; use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::RenderTarget; use crate::render_helpers::RenderTarget;
use crate::utils::round_logical_in_physical;
use crate::utils::transaction::Transaction; use crate::utils::transaction::Transaction;
use crate::utils::{round_logical_in_physical, round_logical_in_physical_max1};
/// Toplevel window with decorations. /// Toplevel window with decorations.
#[derive(Debug)] #[derive(Debug)]
@@ -106,6 +106,11 @@ pub struct Tile<W: LayoutElement> {
/// Scale of the output the tile is on (and rounds its sizes to). /// Scale of the output the tile is on (and rounds its sizes to).
scale: f64, scale: f64,
/// Extra scale used for rendering.
///
/// Applied on top of `scale` and used for visuals only (does not affect the layout).
extra_overview_scale: f64,
/// Clock for driving animations. /// Clock for driving animations.
pub(super) clock: Clock, pub(super) clock: Clock,
@@ -193,6 +198,7 @@ impl<W: LayoutElement> Tile<W> {
rounded_corner_damage: Default::default(), rounded_corner_damage: Default::default(),
view_size, view_size,
scale, scale,
extra_overview_scale: 1.,
clock, clock,
options, options,
} }
@@ -346,9 +352,17 @@ impl<W: LayoutElement> Tile<W> {
.is_some_and(|alpha| !alpha.anim.is_done()) .is_some_and(|alpha| !alpha.anim.is_done())
} }
pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) { pub fn update_render_elements(
&mut self,
is_active: bool,
view_rect: Rectangle<f64, Logical>,
extra_overview_scale: f64,
) {
let rules = self.window.rules(); let rules = self.window.rules();
self.extra_overview_scale = extra_overview_scale;
let visual_scale = self.scale * extra_overview_scale;
let draw_border_with_background = rules let draw_border_with_background = rules
.draw_border_with_background .draw_border_with_background
.unwrap_or_else(|| !self.window.has_ssd()); .unwrap_or_else(|| !self.window.has_ssd());
@@ -371,7 +385,7 @@ impl<W: LayoutElement> Tile<W> {
view_rect.size, view_rect.size,
), ),
radius, radius,
self.scale, visual_scale,
1., 1.,
); );
@@ -386,7 +400,7 @@ impl<W: LayoutElement> Tile<W> {
self.animated_tile_size(), self.animated_tile_size(),
is_active, is_active,
radius, radius,
self.scale, visual_scale,
1., 1.,
); );
@@ -402,7 +416,7 @@ impl<W: LayoutElement> Tile<W> {
!draw_focus_ring_with_background, !draw_focus_ring_with_background,
view_rect, view_rect,
radius, radius,
self.scale, visual_scale,
1., 1.,
); );
} }
@@ -546,6 +560,12 @@ impl<W: LayoutElement> Tile<W> {
Some(self.border.width()) Some(self.border.width())
} }
pub fn visual_effective_border_width(&self) -> Option<f64> {
let visual_scale = self.scale * self.extra_overview_scale;
self.effective_border_width()
.map(move |w| round_logical_in_physical_max1(visual_scale, w))
}
/// Returns the location of the window's visual geometry within this Tile. /// Returns the location of the window's visual geometry within this Tile.
pub fn window_loc(&self) -> Point<f64, Logical> { pub fn window_loc(&self) -> Point<f64, Logical> {
let mut loc = Point::from((0., 0.)); let mut loc = Point::from((0., 0.));
@@ -577,6 +597,38 @@ impl<W: LayoutElement> Tile<W> {
loc loc
} }
pub fn visual_window_loc(&self) -> Point<f64, Logical> {
let mut loc = Point::from((0., 0.));
let visual_scale = self.scale * self.extra_overview_scale;
// In fullscreen, center the window in the given size.
if self.is_fullscreen {
let window_size = self.window_size();
let target_size = self.view_size;
// Windows aren't supposed to be larger than the fullscreen size, but in case we get
// one, leave it at the top-left as usual.
if window_size.w < target_size.w {
loc.x += (target_size.w - window_size.w) / 2.;
}
if window_size.h < target_size.h {
loc.y += (target_size.h - window_size.h) / 2.;
}
// Round to physical pixels.
loc = loc
.to_physical_precise_round(visual_scale)
.to_logical(visual_scale);
}
if let Some(width) = self.visual_effective_border_width() {
loc += (width, width).into();
}
loc
}
pub fn tile_size(&self) -> Size<f64, Logical> { pub fn tile_size(&self) -> Size<f64, Logical> {
let mut size = self.window_size(); let mut size = self.window_size();
@@ -635,6 +687,8 @@ impl<W: LayoutElement> Tile<W> {
pub fn animated_window_size(&self) -> Size<f64, Logical> { pub fn animated_window_size(&self) -> Size<f64, Logical> {
let mut size = self.window_size(); let mut size = self.window_size();
let visual_scale = self.scale * self.extra_overview_scale;
if let Some(resize) = &self.resize_animation { if let Some(resize) = &self.resize_animation {
let val = resize.anim.value(); let val = resize.anim.value();
let size_from = resize.size_from.to_f64(); let size_from = resize.size_from.to_f64();
@@ -642,8 +696,8 @@ impl<W: LayoutElement> Tile<W> {
size.w = f64::max(1., size_from.w + (size.w - size_from.w) * val); size.w = f64::max(1., size_from.w + (size.w - size_from.w) * val);
size.h = f64::max(1., size_from.h + (size.h - size_from.h) * val); size.h = f64::max(1., size_from.h + (size.h - size_from.h) * val);
size = size size = size
.to_physical_precise_round(self.scale) .to_physical_precise_round(visual_scale)
.to_logical(self.scale); .to_logical(visual_scale);
} }
size size
@@ -652,15 +706,20 @@ impl<W: LayoutElement> Tile<W> {
pub fn animated_tile_size(&self) -> Size<f64, Logical> { pub fn animated_tile_size(&self) -> Size<f64, Logical> {
let mut size = self.animated_window_size(); let mut size = self.animated_window_size();
let visual_scale = self.scale * self.extra_overview_scale;
if self.is_fullscreen { if self.is_fullscreen {
// Normally we'd just return the fullscreen size here, but this makes things a bit // Normally we'd just return the fullscreen size here, but this makes things a bit
// nicer if a fullscreen window is bigger than the fullscreen size for some reason. // nicer if a fullscreen window is bigger than the fullscreen size for some reason.
size.w = f64::max(size.w, self.view_size.w); size.w = f64::max(size.w, self.view_size.w);
size.h = f64::max(size.h, self.view_size.h); size.h = f64::max(size.h, self.view_size.h);
size = size
.to_physical_precise_round(visual_scale)
.to_logical(visual_scale);
return size; return size;
} }
if let Some(width) = self.effective_border_width() { if let Some(width) = self.visual_effective_border_width() {
size.w += width * 2.; size.w += width * 2.;
size.h += width * 2.; size.h += width * 2.;
} }
@@ -798,10 +857,12 @@ impl<W: LayoutElement> Tile<W> {
return Point::from((0., 0.)); return Point::from((0., 0.));
} }
let visual_scale = self.scale * self.extra_overview_scale;
let now = self.clock.now().as_secs_f64(); let now = self.clock.now().as_secs_f64();
let amplitude = self.view_size.h / 96.; let amplitude = self.view_size.h / 96.;
let y = amplitude * ((f64::consts::TAU * now / 3.6).sin() - 1.); let y = amplitude * ((f64::consts::TAU * now / 3.6).sin() - 1.);
let y = round_logical_in_physical(self.scale, y); let y = round_logical_in_physical(visual_scale, y);
Point::from((0., y)) Point::from((0., y))
} }
@@ -826,6 +887,7 @@ impl<W: LayoutElement> Tile<W> {
let _span = tracy_client::span!("Tile::render_inner"); let _span = tracy_client::span!("Tile::render_inner");
let scale = Scale::from(self.scale); let scale = Scale::from(self.scale);
let visual_scale = scale * self.extra_overview_scale;
let win_alpha = if self.is_fullscreen || self.window.is_ignoring_opacity_window_rule() { let win_alpha = if self.is_fullscreen || self.window.is_ignoring_opacity_window_rule() {
1. 1.
@@ -843,7 +905,7 @@ impl<W: LayoutElement> Tile<W> {
// passed to update_render_elements(). But, it works well enough for what it is. // passed to update_render_elements(). But, it works well enough for what it is.
let location = location + self.bob_offset(); let location = location + self.bob_offset();
let window_loc = self.window_loc(); let window_loc = self.visual_window_loc();
let window_size = self.window_size().to_f64(); let window_size = self.window_size().to_f64();
let animated_window_size = self.animated_window_size(); let animated_window_size = self.animated_window_size();
let window_render_loc = location + window_loc; let window_render_loc = location + window_loc;
@@ -1035,7 +1097,7 @@ impl<W: LayoutElement> Tile<W> {
}); });
let rv = rv.chain(elem); let rv = rv.chain(elem);
let elem = self.effective_border_width().map(|width| { let elem = self.visual_effective_border_width().map(|width| {
self.border self.border
.render(renderer, location + Point::from((width, width))) .render(renderer, location + Point::from((width, width)))
.map(Into::into) .map(Into::into)
+78 -12
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};
@@ -18,6 +21,7 @@ use super::scrolling::{
Column, ColumnWidth, InsertHint, InsertPosition, ScrollDirection, ScrollingSpace, Column, ColumnWidth, InsertHint, InsertPosition, ScrollDirection, ScrollingSpace,
ScrollingSpaceRenderElement, ScrollingSpaceRenderElement,
}; };
use super::shadow::Shadow;
use super::tile::{Tile, TileRenderSnapshot}; use super::tile::{Tile, TileRenderSnapshot};
use super::{ use super::{
ActivateWindow, HitType, InteractiveResizeData, LayoutElement, Options, RemovedTile, SizeFrac, ActivateWindow, HitType, InteractiveResizeData, LayoutElement, Options, RemovedTile, SizeFrac,
@@ -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,
@@ -336,13 +368,34 @@ impl<W: LayoutElement> Workspace<W> {
self.scrolling.are_transitions_ongoing() || self.floating.are_transitions_ongoing() self.scrolling.are_transitions_ongoing() || self.floating.are_transitions_ongoing()
} }
pub fn update_render_elements(&mut self, is_active: bool) { pub fn update_render_elements(
self.scrolling &mut self,
.update_render_elements(is_active && !self.floating_is_active.get()); is_active: bool,
is_overview_open: bool,
extra_overview_scale: f64,
) {
self.scrolling.update_render_elements(
is_active && !self.floating_is_active.get(),
is_overview_open,
extra_overview_scale,
);
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(
.update_render_elements(is_active && self.floating_is_active.get(), view_rect); is_active && self.floating_is_active.get(),
view_rect,
extra_overview_scale,
);
let visual_scale = self.scale.fractional_scale() * extra_overview_scale;
self.shadow.update_render_elements(
self.view_size,
true,
CornerRadius::default(),
visual_scale,
1.,
);
} }
pub fn update_config(&mut self, base_options: Rc<Options>) { pub fn update_config(&mut self, base_options: Rc<Options>) {
@@ -370,6 +423,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> + '_ {
@@ -1409,11 +1463,15 @@ impl<W: LayoutElement> Workspace<W> {
renderer: &mut R, renderer: &mut R,
target: RenderTarget, target: RenderTarget,
focus_ring: bool, focus_ring: bool,
is_overview_open: bool,
) -> impl Iterator<Item = WorkspaceRenderElement<R>> { ) -> impl Iterator<Item = WorkspaceRenderElement<R>> {
let scrolling_focus_ring = focus_ring && !self.floating_is_active(); let scrolling_focus_ring = focus_ring && !self.floating_is_active();
let scrolling = self let scrolling = self.scrolling.render_elements(
.scrolling renderer,
.render_elements(renderer, target, scrolling_focus_ring); target,
scrolling_focus_ring,
is_overview_open,
);
let scrolling = scrolling.into_iter().map(WorkspaceRenderElement::from); let scrolling = scrolling.into_iter().map(WorkspaceRenderElement::from);
let floating_focus_ring = focus_ring && self.floating_is_active(); let floating_focus_ring = focus_ring && self.floating_is_active();
@@ -1428,6 +1486,13 @@ impl<W: LayoutElement> Workspace<W> {
floating.into_iter().flatten().chain(scrolling) floating.into_iter().flatten().chain(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()
} }
@@ -1446,7 +1511,7 @@ impl<W: LayoutElement> Workspace<W> {
if tile.window().id() == window { if tile.window().id() == window {
let view_pos = Point::from((-tile_pos.x, -tile_pos.y)); let view_pos = Point::from((-tile_pos.x, -tile_pos.y));
let view_rect = Rectangle::new(view_pos, view_size); let view_rect = Rectangle::new(view_pos, view_size);
tile.update_render_elements(false, view_rect); tile.update_render_elements(false, view_rect, 1.);
tile.store_unmap_snapshot_if_empty(renderer); tile.store_unmap_snapshot_if_empty(renderer);
return; return;
} }
@@ -1628,7 +1693,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) -> bool {
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;
@@ -1654,8 +1719,9 @@ 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)
} }
pub fn dnd_scroll_gesture_end(&mut self) { pub fn dnd_scroll_gesture_end(&mut self) {
+205 -45
View File
@@ -15,7 +15,7 @@ use anyhow::{bail, ensure, Context};
use calloop::futures::Scheduler; use calloop::futures::Scheduler;
use niri_config::{ use niri_config::{
Config, FloatOrInt, Key, Modifiers, OutputName, PreviewRender, TrackLayout, Config, FloatOrInt, Key, Modifiers, OutputName, PreviewRender, TrackLayout,
WarpMouseToFocusMode, WorkspaceReference, DEFAULT_BACKGROUND_COLOR, WarpMouseToFocusMode, WorkspaceReference, DEFAULT_BACKDROP_COLOR, DEFAULT_BACKGROUND_COLOR,
}; };
use smithay::backend::allocator::Fourcc; use smithay::backend::allocator::Fourcc;
use smithay::backend::input::Keycode; use smithay::backend::input::Keycode;
@@ -26,7 +26,8 @@ use smithay::backend::renderer::element::surface::{
render_elements_from_surface_tree, WaylandSurfaceRenderElement, render_elements_from_surface_tree, WaylandSurfaceRenderElement,
}; };
use smithay::backend::renderer::element::utils::{ use smithay::backend::renderer::element::utils::{
select_dmabuf_feedback, Relocate, RelocateRenderElement, select_dmabuf_feedback, CropRenderElement, Relocate, RelocateRenderElement,
RescaleRenderElement,
}; };
use smithay::backend::renderer::element::{ use smithay::backend::renderer::element::{
default_primary_scanout_output_compare, Id, Kind, PrimaryScanoutOutput, RenderElementStates, default_primary_scanout_output_compare, Id, Kind, PrimaryScanoutOutput, RenderElementStates,
@@ -131,7 +132,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};
@@ -344,6 +345,7 @@ pub struct Niri {
/// Used for limiting the notify to once per iteration, so that it's not spammed with high /// Used for limiting the notify to once per iteration, so that it's not spammed with high
/// resolution mice. /// resolution mice.
pub notified_activity_this_iteration: bool, pub notified_activity_this_iteration: bool,
pub pointer_inside_hot_corner: bool,
pub tablet_cursor_location: Option<Point<f64, Logical>>, pub tablet_cursor_location: Option<Point<f64, Logical>>,
pub gesture_swipe_3f_cumulative: Option<(f64, f64)>, pub gesture_swipe_3f_cumulative: Option<(f64, f64)>,
pub vertical_wheel_tracker: ScrollTracker, pub vertical_wheel_tracker: ScrollTracker,
@@ -428,6 +430,7 @@ pub struct OutputState {
/// Solid color buffer for the background that we use instead of clearing to avoid damage /// Solid color buffer for the background that we use instead of clearing to avoid damage
/// tracking issues and make screenshots easier. /// tracking issues and make screenshots easier.
pub background_buffer: SolidColorBuffer, pub background_buffer: SolidColorBuffer,
pub backdrop_buffer: SolidColorBuffer,
pub lock_render_state: LockRenderState, pub lock_render_state: LockRenderState,
pub lock_surface: Option<LockSurface>, pub lock_surface: Option<LockSurface>,
pub lock_color_buffer: SolidColorBuffer, pub lock_color_buffer: SolidColorBuffer,
@@ -467,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)]
@@ -563,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,
} }
} }
@@ -572,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 {
@@ -1040,6 +1050,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 self.niri.layout.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);
@@ -2392,6 +2407,7 @@ impl Niri {
pointer_inactivity_timer: None, pointer_inactivity_timer: None,
pointer_inactivity_timer_got_reset: false, pointer_inactivity_timer_got_reset: false,
notified_activity_this_iteration: false, notified_activity_this_iteration: false,
pointer_inside_hot_corner: false,
tablet_cursor_location: None, tablet_cursor_location: None,
gesture_swipe_3f_cumulative: None, gesture_swipe_3f_cumulative: None,
vertical_wheel_tracker: ScrollTracker::new(120), vertical_wheel_tracker: ScrollTracker::new(120),
@@ -2640,6 +2656,9 @@ impl Niri {
.to_array_unpremul(); .to_array_unpremul();
background_color[3] = 1.; background_color[3] = 1.;
let mut backdrop_color = DEFAULT_BACKDROP_COLOR.to_array_unpremul();
backdrop_color[3] = 1.;
// FIXME: fix winit damage on other transforms. // FIXME: fix winit damage on other transforms.
if name.connector == "winit" { if name.connector == "winit" {
transform = Transform::Flipped180; transform = Transform::Flipped180;
@@ -2673,6 +2692,7 @@ impl Niri {
last_drm_sequence: None, last_drm_sequence: None,
frame_callback_sequence: 0, frame_callback_sequence: 0,
background_buffer: SolidColorBuffer::new(size, background_color), background_buffer: SolidColorBuffer::new(size, background_color),
backdrop_buffer: SolidColorBuffer::new(size, backdrop_color),
lock_render_state, lock_render_state,
lock_surface: None, lock_surface: None,
lock_color_buffer: SolidColorBuffer::new(size, CLEAR_COLOR_LOCKED), lock_color_buffer: SolidColorBuffer::new(size, CLEAR_COLOR_LOCKED),
@@ -2777,6 +2797,7 @@ impl Niri {
if let Some(state) = self.output_state.get_mut(output) { if let Some(state) = self.output_state.get_mut(output) {
state.background_buffer.resize(output_size); state.background_buffer.resize(output_size);
state.backdrop_buffer.resize(output_size);
state.lock_color_buffer.resize(output_size); state.lock_color_buffer.resize(output_size);
if let Some(lock_surface) = &state.lock_surface { if let Some(lock_surface) = &state.lock_surface {
@@ -2837,17 +2858,11 @@ impl Niri {
Some((output, pos_within_output)) Some((output, pos_within_output))
} }
/// Returns the window under the position to be activated. pub fn is_layout_obscured_under(
/// &self,
/// The cursor may be inside the window's activation region, but not within the window's input output: &Output,
/// region. pos_within_output: Point<f64, Logical>,
pub fn window_under(&self, pos: Point<f64, Logical>) -> Option<&Mapped> { ) -> bool {
if self.is_locked() || self.screenshot_ui.is_open() {
return None;
}
let (output, pos_within_output) = self.output_under(pos)?;
// The ordering here must be consistent with the ordering in render() so that input is // The ordering here must be consistent with the ordering in render() so that input is
// consistent with the visuals. // consistent with the visuals.
@@ -2874,16 +2889,78 @@ impl Niri {
let layer_popup_under = |layer| layer_surface_under(layer, true); let layer_popup_under = |layer| layer_surface_under(layer, true);
if layer_popup_under(Layer::Overlay) || layer_toplevel_under(Layer::Overlay) { if layer_popup_under(Layer::Overlay) || layer_toplevel_under(Layer::Overlay) {
return None; return true;
} }
let mon = self.layout.monitor_for_output(output).unwrap(); let mon = self.layout.monitor_for_output(output).unwrap();
if !mon.render_above_top_layer() if mon.render_above_top_layer() {
&& (layer_popup_under(Layer::Top) return false;
|| layer_popup_under(Layer::Bottom) }
|| layer_popup_under(Layer::Background)
|| layer_toplevel_under(Layer::Top)) let hot_corner = Rectangle::from_size(Size::from((1., 1.)));
{ if hot_corner.contains(pos_within_output) {
return true;
}
if layer_popup_under(Layer::Top) || layer_toplevel_under(Layer::Top) {
return true;
}
if self.layout.is_overview_open() {
return false;
}
if layer_popup_under(Layer::Bottom) || layer_popup_under(Layer::Background) {
return true;
}
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_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
/// region.
pub fn window_under(&self, pos: Point<f64, Logical>) -> Option<&Mapped> {
if self.is_locked() || self.screenshot_ui.is_open() {
return None;
}
let (output, pos_within_output) = self.output_under(pos)?;
if self.is_layout_obscured_under(output, pos_within_output) {
return None; return None;
} }
@@ -3002,26 +3079,42 @@ 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() {
under = under under = under
.or_else(window_under) .or_else(window_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_popup_under(Layer::Bottom)) .or_else(|| layer_popup_under(Layer::Bottom))
.or_else(|| layer_popup_under(Layer::Background)) .or_else(|| layer_popup_under(Layer::Background))
.or_else(|| layer_toplevel_under(Layer::Top))
.or_else(|| layer_toplevel_under(Layer::Bottom)) .or_else(|| layer_toplevel_under(Layer::Bottom))
.or_else(|| layer_toplevel_under(Layer::Background)); .or_else(|| layer_toplevel_under(Layer::Background));
} else { } else {
let hot_corner = Rectangle::from_size(Size::from((1., 1.)));
if hot_corner.contains(pos_within_output) {
return rv;
}
under = under under = under
.or_else(|| layer_popup_under(Layer::Top)) .or_else(|| layer_popup_under(Layer::Top))
.or_else(|| layer_popup_under(Layer::Bottom)) .or_else(|| layer_toplevel_under(Layer::Top));
.or_else(|| layer_popup_under(Layer::Background))
.or_else(|| layer_toplevel_under(Layer::Top)) if !is_overview_open {
.or_else(window_under) under = under
.or_else(|| layer_toplevel_under(Layer::Bottom)) .or_else(|| layer_popup_under(Layer::Bottom))
.or_else(|| layer_toplevel_under(Layer::Background)); .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 {
@@ -3480,6 +3573,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);
@@ -3733,10 +3827,18 @@ impl Niri {
return elements; return elements;
} }
// Prepare the background element. // Prepare the background elements.
let state = self.output_state.get(output).unwrap(); let state = self.output_state.get(output).unwrap();
let background_buffer = state.background_buffer.clone();
let background = SolidColorRenderElement::from_buffer( let background = SolidColorRenderElement::from_buffer(
&state.background_buffer, &background_buffer,
(0, 0),
output_scale,
1.,
Kind::Unspecified,
);
let backdrop = SolidColorRenderElement::from_buffer(
&state.backdrop_buffer,
(0, 0), (0, 0),
output_scale, output_scale,
1., 1.,
@@ -3753,8 +3855,8 @@ impl Niri {
.map(OutputRenderElements::from), .map(OutputRenderElements::from),
); );
// Add the background for outputs that were connected while the screenshot UI was open. // Add the backdrop for outputs that were connected while the screenshot UI was open.
elements.push(background); elements.push(backdrop);
if self.debug_draw_opaque_regions { if self.debug_draw_opaque_regions {
draw_opaque_regions(&mut elements, output_scale); draw_opaque_regions(&mut elements, output_scale);
@@ -3773,7 +3875,11 @@ 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 monitor_elements: Vec<_> = mon.render_elements(renderer, target, focus_ring).collect(); let ws_scale = mon.workspace_scale();
let monitor_elements = Vec::from_iter(
mon.render_elements(renderer, target, focus_ring)
.map(|(geo, iter)| (geo, Vec::from_iter(iter))),
);
let int_move_elements: Vec<_> = self let int_move_elements: Vec<_> = self
.layout .layout
.render_interactive_move_for_output(renderer, output, target) .render_interactive_move_for_output(renderer, output, target)
@@ -3791,10 +3897,13 @@ impl Niri {
extend_from_layer(&mut layer_elems, Layer::Overlay); extend_from_layer(&mut layer_elems, Layer::Overlay);
elements.extend(layer_elems.into_iter().map(OutputRenderElements::from)); elements.extend(layer_elems.into_iter().map(OutputRenderElements::from));
// Collect all other layer-shell elements. // Collect the top layer elements.
let mut layer_elems = SplitElements::default(); let mut layer_elems = SplitElements::default();
extend_from_layer(&mut layer_elems, Layer::Top); extend_from_layer(&mut layer_elems, Layer::Top);
let top_layer_normal = mem::take(&mut layer_elems.normal); let top_layer = layer_elems;
// Collect all other layer-shell elements.
let mut layer_elems = SplitElements::default();
extend_from_layer(&mut layer_elems, Layer::Bottom); extend_from_layer(&mut layer_elems, Layer::Bottom);
extend_from_layer(&mut layer_elems, Layer::Background); extend_from_layer(&mut layer_elems, Layer::Background);
@@ -3806,27 +3915,71 @@ impl Niri {
.into_iter() .into_iter()
.map(OutputRenderElements::from), .map(OutputRenderElements::from),
); );
elements.extend(monitor_elements.into_iter().map(OutputRenderElements::from)); elements.extend(
monitor_elements
.into_iter()
.flat_map(|(_, iter)| iter)
.map(OutputRenderElements::from),
);
elements.extend(top_layer.into_iter().map(OutputRenderElements::from));
elements.extend(layer_elems.popups.drain(..).map(OutputRenderElements::from)); elements.extend(layer_elems.popups.drain(..).map(OutputRenderElements::from));
elements.extend(top_layer_normal.into_iter().map(OutputRenderElements::from));
elements.extend(layer_elems.normal.drain(..).map(OutputRenderElements::from)); elements.extend(layer_elems.normal.drain(..).map(OutputRenderElements::from));
} else {
elements.extend(layer_elems.popups.drain(..).map(OutputRenderElements::from));
elements.extend(top_layer_normal.into_iter().map(OutputRenderElements::from));
// TODO background
} else {
elements.extend(top_layer.into_iter().map(OutputRenderElements::from));
// TODO: adjust input to put interactive move above popups.
elements.extend( elements.extend(
int_move_elements int_move_elements
.into_iter() .into_iter()
.map(OutputRenderElements::from), .map(OutputRenderElements::from),
); );
elements.extend(monitor_elements.into_iter().map(OutputRenderElements::from));
elements.extend(layer_elems.normal.drain(..).map(OutputRenderElements::from)); for (ws_geo, ws_elements) in monitor_elements {
// Collect all other layer-shell elements.
let mut layer_elems = SplitElements::default();
extend_from_layer(&mut layer_elems, Layer::Bottom);
extend_from_layer(&mut layer_elems, Layer::Background);
let ws_geo = ws_geo.to_physical_precise_round(output_scale);
for elem in layer_elems.popups {
let elem =
RescaleRenderElement::from_element(elem, Point::from((0, 0)), ws_scale);
let elem =
RelocateRenderElement::from_element(elem, ws_geo.loc, Relocate::Relative);
if let Some(elem) = CropRenderElement::from_element(elem, output_scale, ws_geo)
{
elements.push(OutputRenderElements::from(elem));
}
}
elements.extend(ws_elements.into_iter().map(OutputRenderElements::from));
for elem in layer_elems.normal {
let elem =
RescaleRenderElement::from_element(elem, Point::from((0, 0)), ws_scale);
let elem =
RelocateRenderElement::from_element(elem, ws_geo.loc, Relocate::Relative);
if let Some(elem) = CropRenderElement::from_element(elem, output_scale, ws_geo)
{
elements.push(OutputRenderElements::from(elem));
}
}
let elem = background.clone();
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), ws_scale);
let elem =
RelocateRenderElement::from_element(elem, ws_geo.loc, Relocate::Relative);
if let Some(elem) = CropRenderElement::from_element(elem, output_scale, ws_geo) {
elements.push(OutputRenderElements::from(elem));
}
}
} }
// Then the background. // Then the backdrop.
elements.push(background); elements.push(backdrop);
if self.debug_draw_opaque_regions { if self.debug_draw_opaque_regions {
draw_opaque_regions(&mut elements, output_scale); draw_opaque_regions(&mut elements, output_scale);
@@ -5415,7 +5568,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.
@@ -5653,10 +5806,17 @@ niri_render_elements! {
OutputRenderElements<R> => { OutputRenderElements<R> => {
Monitor = MonitorRenderElement<R>, Monitor = MonitorRenderElement<R>,
Tile = TileRenderElement<R>, Tile = TileRenderElement<R>,
RescaledTile = RescaleRenderElement<TileRenderElement<R>>,
LayerSurface = LayerSurfaceRenderElement<R>, LayerSurface = LayerSurfaceRenderElement<R>,
RelocatedLayerSurface = CropRenderElement<RelocateRenderElement<RescaleRenderElement<
LayerSurfaceRenderElement<R>
>>>,
Wayland = WaylandSurfaceRenderElement<R>, Wayland = WaylandSurfaceRenderElement<R>,
NamedPointer = MemoryRenderBufferRenderElement<R>, NamedPointer = MemoryRenderBufferRenderElement<R>,
SolidColor = SolidColorRenderElement, SolidColor = SolidColorRenderElement,
RelocatedSolidColor = CropRenderElement<RelocateRenderElement<RescaleRenderElement<
SolidColorRenderElement
>>>,
ScreenshotUi = ScreenshotUiRenderElement, ScreenshotUi = ScreenshotUiRenderElement,
Texture = PrimaryGpuTextureRenderElement, Texture = PrimaryGpuTextureRenderElement,
// Used for the CPU-rendered panels. // Used for the CPU-rendered panels.
+5
View File
@@ -245,6 +245,11 @@ impl ShaderRenderElement {
self.area.loc = location; self.area.loc = location;
self self
} }
pub fn with_alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha;
self
}
} }
impl Element for ShaderRenderElement { impl Element for ShaderRenderElement {
+5
View File
@@ -175,6 +175,11 @@ impl ShadowRenderElement {
self self
} }
pub fn with_alpha(mut self, alpha: f32) -> Self {
self.inner = self.inner.with_alpha(alpha);
self
}
pub fn has_shader(renderer: &mut impl NiriRenderer) -> bool { pub fn has_shader(renderer: &mut impl NiriRenderer) -> bool {
Shaders::get(renderer) Shaders::get(renderer)
.program(ProgramType::Shadow) .program(ProgramType::Shadow)