This commit is contained in:
Ivan Molodetskikh
2025-04-15 09:13:54 +03:00
parent 43c1592ab7
commit cc2549323d
15 changed files with 1723 additions and 209 deletions

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::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;
@@ -984,6 +985,8 @@ pub struct Animations {
pub config_notification_open_close: ConfigNotificationOpenCloseAnim,
#[knuffel(child, default)]
pub screenshot_ui_open: ScreenshotUiOpenAnim,
#[knuffel(child, default)]
pub overview_open_close: OverviewOpenCloseAnim,
}
impl Default for Animations {
@@ -999,6 +1002,7 @@ impl Default for Animations {
window_resize: Default::default(),
config_notification_open_close: Default::default(),
screenshot_ui_open: Default::default(),
overview_open_close: Default::default(),
}
}
}
@@ -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)]
pub struct Animation {
pub off: bool,
@@ -1183,6 +1203,8 @@ pub struct SpringParams {
pub struct Gestures {
#[knuffel(child, default)]
pub dnd_edge_view_scroll: DndEdgeViewScroll,
#[knuffel(child, default)]
pub dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch,
}
#[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)]
pub struct Environment(#[knuffel(children)] pub Vec<EnvironmentVariable>);
@@ -1716,6 +1758,7 @@ pub enum Action {
SetDynamicCastWindowById(u64),
SetDynamicCastMonitor(#[knuffel(argument)] Option<String>),
ClearDynamicCastTarget,
ToggleOverview,
}
impl From<niri_ipc::Action> for Action {
@@ -1980,6 +2023,7 @@ impl From<niri_ipc::Action> for Action {
Self::SetDynamicCastMonitor(output)
}
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 {
pub fn new_off() -> 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 {
dnd_edge_view_scroll: DndEdgeViewScroll {
@@ -4470,6 +4541,15 @@ mod tests {
50.0,
),
},
dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch {
trigger_height: FloatOrInt(
50.0,
),
delay_ms: 100,
max_speed: FloatOrInt(
1500.0,
),
},
},
environment: Environment(
[

View File

@@ -764,6 +764,8 @@ pub enum Action {
},
/// Clear the dynamic cast target, making it show nothing.
ClearDynamicCastTarget {},
/// Toggle the Overview.
ToggleOverview {},
}
/// Change in window or column size.

View File

@@ -266,6 +266,7 @@ impl TestCase for Layout {
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer, RenderTarget::Output, true)
.flat_map(|(_, iter)| iter)
.map(|elem| Box::new(elem) as _)
.collect()
}

View File

@@ -153,7 +153,7 @@ impl XdgShellHandler for State {
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = MoveGrab::new(start_data, window);
let grab = MoveGrab::new(start_data, window, false);
pointer.set_grab(self, grab, serial, Focus::Clear);
}
PointerOrTouchStartData::Touch(start_data) => {
@@ -316,6 +316,9 @@ impl XdgShellHandler for State {
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
// FIXME: somewhere here we probably need to check is_overview_open to match the logic
// in update_keyboard_focus().
if layers
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
.is_none()

View File

@@ -29,7 +29,7 @@ use smithay::input::touch::{
};
use smithay::input::SeatHandler;
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::pointer_constraints::{with_pointer_constraint, PointerConstraint};
use smithay::wayland::selection::data_device::DnDGrab;
@@ -393,6 +393,15 @@ impl State {
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;
should_intercept_key(
&mut this.niri.suppressed_keys,
@@ -1915,10 +1924,18 @@ impl State {
Action::ClearDynamicCastTarget => {
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) {
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.
if self.niri.global_space.outputs().next().is_none() {
return;
@@ -2095,6 +2112,18 @@ impl State {
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.
self.niri.maybe_activate_pointer_constraint();
@@ -2119,6 +2148,10 @@ impl State {
&mut self,
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(|| {
self.global_bounding_rectangle().map(|output_geo| {
event.position_transformed(output_geo.size) + output_geo.loc.to_f64()
@@ -2164,6 +2197,18 @@ impl State {
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();
// We moved the pointer, show it.
@@ -2235,10 +2280,54 @@ impl State {
self.niri.pointer_hidden = false;
self.niri.tablet_cursor_location = None;
let is_overview_open = self.niri.layout.is_overview_open();
if is_overview_open && !pointer.is_grabbed() && button == Some(MouseButton::Right) {
if let Some((output, ws)) = self.niri.workspace_under_cursor(true) {
let ws_id = ws.id();
let ws_idx = self.niri.layout.find_workspace_by_id(ws_id).unwrap().0;
self.niri.layout.focus_output(&output);
let location = pointer.current_location();
let start_data = PointerGrabStartData {
focus: None,
button: button_code,
location,
};
self.niri
.layout
.view_offset_gesture_begin(&output, Some(ws_idx), false);
let grab = SpatialMovementGrab::new(start_data, output, ws_id, true);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::AllScroll));
// FIXME: granular.
self.niri.queue_redraw_all();
// 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() {
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
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);
let location = pointer.current_location();
@@ -2247,7 +2336,7 @@ impl State {
button: button_code,
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);
self.niri
.cursor_manager
@@ -2269,12 +2358,14 @@ impl State {
// Check if we need to start an interactive move.
if button == Some(MouseButton::Left) && !pointer.is_grabbed() {
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
if mod_down {
if is_overview_open || mod_down {
let location = pointer.current_location();
let (output, pos_within_output) = self.niri.output_under(location).unwrap();
let output = output.clone();
self.niri.layout.activate_window(&window);
if !is_overview_open {
self.niri.layout.activate_window(&window);
}
if self.niri.layout.interactive_move_begin(
window.clone(),
@@ -2286,11 +2377,14 @@ impl State {
button: button_code,
location,
};
let grab = MoveGrab::new(start_data, window.clone());
let grab = MoveGrab::new(start_data, window.clone(), is_overview_open);
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
if !is_overview_open {
self.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
}
}
}
}
@@ -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.
self.niri.queue_redraw_all();
@@ -2437,23 +2544,63 @@ impl State {
let horizontal_amount_v120 = event.amount_v120(Axis::Horizontal);
let vertical_amount_v120 = event.amount_v120(Axis::Vertical);
let is_overview_open = self.niri.layout.is_overview_open();
// Handle wheel scroll bindings.
if source == AxisSource::Wheel {
// 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.
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
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 ticks = self.niri.horizontal_wheel_tracker.accumulate(horizontal);
if ticks != 0 {
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);
drop(config);
let (bind_left, bind_right) = if is_overview_open {
if modifiers.is_empty() {
let bind_left = Some(Bind {
key: Key {
trigger: Trigger::WheelScrollLeft,
modifiers: Modifiers::empty(),
},
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 {
for _ in 0..ticks {
@@ -2470,13 +2617,45 @@ impl State {
let vertical = vertical_amount_v120.unwrap_or(0.);
let ticks = self.niri.vertical_wheel_tracker.accumulate(vertical);
if ticks != 0 {
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);
drop(config);
let (bind_up, bind_down) = if is_overview_open {
if modifiers.is_empty() {
let bind_up = Some(Bind {
key: Key {
trigger: Trigger::WheelScrollUp,
modifiers: Modifiers::empty(),
},
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 {
for _ in 0..ticks {
@@ -2771,6 +2950,12 @@ impl State {
if event.fingers() == 3 {
self.niri.gesture_swipe_3f_cumulative = Some((0., 0.));
// We handled this event.
return;
} else if event.fingers() == 4 {
self.niri.layout.overview_gesture_begin();
self.niri.queue_redraw_all();
// We handled this event.
return;
}
@@ -2816,6 +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 {
*cx += delta_x;
*cy += delta_y;
@@ -2827,7 +3014,21 @@ impl State {
if let Some(output) = self.niri.output_under_cursor() {
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 {
self.niri
.layout
@@ -2862,6 +3063,14 @@ impl State {
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 {
// We handled this event.
return;
@@ -2904,6 +3113,12 @@ impl State {
handled = true;
}
let res = self.niri.layout.overview_gesture_end();
if res {
self.niri.queue_redraw_all();
handled = true;
}
if handled {
// We handled this event.
return;

View File

@@ -1,10 +1,11 @@
use smithay::backend::input::ButtonState;
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
@@ -15,14 +16,32 @@ pub struct MoveGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GestureState {
Recognizing,
Move,
}
impl MoveGrab {
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
pub fn new(
start_data: PointerGrabStartData<State>,
window: Window,
use_threshold: bool,
) -> Self {
let gesture = if use_threshold {
GestureState::Recognizing
} else {
GestureState::Move
};
Self {
last_location: start_data.location,
start_data,
window,
gesture,
}
}
@@ -53,6 +72,24 @@ impl PointerGrab<State> for MoveGrab {
let output = output.clone();
let event_delta = event.location - self.last_location;
self.last_location = event.location;
if self.gesture == GestureState::Recognizing {
let c = event.location - self.start_data.location;
// Check if the gesture moved far enough to decide.
if c.x * c.x + c.y * c.y >= 8. * 8. {
self.gesture = GestureState::Move;
data.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
}
}
if self.gesture != GestureState::Move {
return;
}
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,

View File

@@ -10,12 +10,14 @@ use smithay::input::SeatHandler;
use smithay::output::Output;
use smithay::utils::{Logical, Point};
use crate::layout::workspace::WorkspaceId;
use crate::niri::State;
pub struct SpatialMovementGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
output: Output,
workspace_id: WorkspaceId,
gesture: GestureState,
}
@@ -27,12 +29,24 @@ enum GestureState {
}
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 {
last_location: start_data.location,
start_data,
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.abs() > c.y.abs() {
self.gesture = GestureState::ViewOffset;
layout.view_offset_gesture_begin(&self.output, false);
layout.view_offset_gesture_update(-c.x, timestamp, false)
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(self.workspace_id) {
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 {
self.gesture = GestureState::WorkspaceSwitch;
layout.workspace_switch_gesture_begin(&self.output, false);

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ use std::rc::Rc;
use std::time::Duration;
use smithay::backend::renderer::element::utils::{
CropRenderElement, Relocate, RelocateRenderElement,
CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Size};
@@ -14,14 +14,16 @@ use super::tile::Tile;
use super::workspace::{
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::input::swipe_tracker::SwipeTracker;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::RenderTarget;
use crate::rubber_band::RubberBand;
use crate::utils::transaction::Transaction;
use crate::utils::{output_size, ResizeEdge};
use crate::utils::{output_size, round_logical_in_physical, ResizeEdge};
/// Amount of touchpad movement to scroll the height of one workspace.
const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.;
@@ -31,6 +33,11 @@ const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
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)]
pub struct Monitor<W: LayoutElement> {
/// Output for this monitor.
@@ -45,6 +52,10 @@ pub struct Monitor<W: LayoutElement> {
pub(super) previous_workspace_id: Option<WorkspaceId>,
/// In-progress switch between workspaces.
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.
pub(super) clock: Clock,
/// Configurable properties of the layout.
@@ -66,6 +77,22 @@ pub struct WorkspaceSwitchGesture {
tracker: SwipeTracker,
/// Whether the gesture is controlled by the touchpad.
is_touchpad: bool,
/// Whether the gesture is clamped to +-1 workspace around the center.
is_clamped: bool,
// 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.
@@ -85,8 +112,13 @@ pub enum MonitorAddWindowTarget<'a, W: LayoutElement> {
NextTo(&'a W::Id),
}
pub type MonitorRenderElement<R> =
RelocateRenderElement<CropRenderElement<WorkspaceRenderElement<R>>>;
niri_render_elements! {
MonitorRenderElement<R> => {
Workspace = RelocateRenderElement<RescaleRenderElement<CropRenderElement<
WorkspaceRenderElement<R>>>>,
Shadow = RelocateRenderElement<RescaleRenderElement<ShadowRenderElement>>,
}
}
impl WorkspaceSwitch {
pub fn current_idx(&self) -> f64 {
@@ -126,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> {
pub fn new(
output: Output,
@@ -139,6 +196,8 @@ impl<W: LayoutElement> Monitor<W> {
workspaces,
active_workspace_idx: 0,
previous_workspace_id: None,
overview_open: false,
overview_progress: None,
workspace_switch: None,
clock,
options,
@@ -212,17 +271,21 @@ impl<W: LayoutElement> Monitor<W> {
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 {
return;
}
// FIXME: also compute and use current velocity.
let current_idx = self
.workspace_switch
.as_ref()
.map(|s| s.current_idx())
.unwrap_or(self.active_workspace_idx as f64);
let current_idx = self.workspace_render_idx();
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
@@ -233,7 +296,7 @@ impl<W: LayoutElement> Monitor<W> {
current_idx,
idx as f64,
0.,
self.options.animations.workspace_switch.0,
config.unwrap_or(self.options.animations.workspace_switch.0),
)));
}
@@ -639,11 +702,32 @@ impl<W: LayoutElement> Monitor<W> {
}
pub fn advance_animations(&mut self) {
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
if anim.is_done() {
self.workspace_switch = None;
self.clean_up_workspaces();
match &mut self.workspace_switch {
Some(WorkspaceSwitch::Animation(anim)) => {
if anim.is_done() {
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 {
@@ -667,8 +751,9 @@ impl<W: LayoutElement> Monitor<W> {
}
pub fn update_render_elements(&mut self, is_active: bool) {
let is_overview_open = self.overview_open;
for (ws, _) in self.workspaces_with_render_geo_mut() {
ws.update_render_elements(is_active);
ws.update_render_elements(is_active, is_overview_open);
}
}
@@ -804,6 +889,10 @@ impl<W: LayoutElement> Monitor<W> {
///
/// During animations, assumes the final view position.
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
if self.overview_open {
return None;
}
// TODO: unify logic
let mut rect = self.active_workspace_ref().active_tile_visual_rectangle()?;
if let Some(switch) = &self.workspace_switch {
@@ -819,32 +908,140 @@ impl<W: LayoutElement> Monitor<W> {
Some(rect)
}
pub fn workspaces_render_geo(&self) -> impl Iterator<Item = Rectangle<f64, Logical>> {
let render_idx = if let Some(switch) = &self.workspace_switch {
pub fn workspace_scale(&self) -> f64 {
if let Some(p) = &self.overview_progress {
(1. - p.value() * (1. - OVERVIEW_WORKSPACE_SCALE)).max(0.)
} else {
1.
}
}
pub(super) fn set_overview_progress(&mut self, progress: Option<&super::OverviewProgress>) {
let prev_render_idx = self.workspace_render_idx();
self.overview_progress = progress.map(OverviewProgress::from);
let new_render_idx = self.workspace_render_idx();
// If the view jumped (can happen when going from corrected to uncorrected render_idx, for
// example when toggling the overview in the middle of an overview animation), then restart
// the workspace switch to avoid jumps.
if prev_render_idx != new_render_idx {
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
// FIXME: maintain velocity.
*anim = anim.restarted(prev_render_idx, anim.to(), 0.);
}
}
}
#[cfg(test)]
pub(super) fn overview_progress_value(&self) -> Option<f64> {
self.overview_progress.as_ref().map(|p| p.value())
}
pub fn workspace_render_idx(&self) -> f64 {
// If workspace switch and overview progress are matching animations, then compute a
// correction term to make the movement appear monotonic.
if let (
Some(WorkspaceSwitch::Animation(switch_anim)),
Some(OverviewProgress::Animation(progress_anim)),
) = (&self.workspace_switch, &self.overview_progress)
{
if switch_anim.start_time() == progress_anim.start_time()
&& (switch_anim.duration().as_secs_f64() - progress_anim.duration().as_secs_f64())
.abs()
<= 0.001
{
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
};
let before_idx = render_idx.floor();
}
}
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();
// Compute the offset in such a way that if render_idx is active_workspace_idx, then its
// offset will be (0., 0.).
let before_ws_y = (before_idx - render_idx) * size.h;
// Ceil the workspace size in physical pixels.
let ws_size = Size::from((size.w, size.h))
let ws_size = size
.upscale(ws_scale)
.to_physical_precise_ceil(scale)
.to_logical(scale);
let first_ws_y = before_ws_y - ws_size.h * before_idx;
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_size.h;
let loc = Point::from((0., y));
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)
})
@@ -890,20 +1087,42 @@ impl<W: LayoutElement> Monitor<W> {
Some((ws, geo))
}
pub fn workspace_under_narrow(
&self,
pos_within_output: Point<f64, Logical>,
) -> Option<&Workspace<W>> {
self.workspaces_with_render_geo()
.find_map(|(ws, geo)| geo.contains(pos_within_output).then_some(ws))
}
pub fn window_under(&self, pos_within_output: Point<f64, Logical>) -> Option<(&W, HitType)> {
let (ws, geo) = self.workspace_under(pos_within_output)?;
let (win, hit) = ws.window_under(pos_within_output - geo.loc)?;
Some((win, hit.offset_win_pos(geo.loc)))
if self.overview_progress.is_some() {
let 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> {
if self.overview_progress.is_some() {
return None;
}
let (ws, geo) = self.workspace_under(pos_within_output)?;
ws.resize_edges_under(pos_within_output - geo.loc)
}
pub fn render_above_top_layer(&self) -> bool {
// Render above the top layer only if the view is stationary.
if self.workspace_switch.is_some() {
if self.workspace_switch.is_some() || self.overview_progress.is_some() {
return false;
}
@@ -916,7 +1135,12 @@ impl<W: LayoutElement> Monitor<W> {
renderer: &'a mut R,
target: RenderTarget,
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 scale = self.output.current_scale().fractional_scale();
@@ -934,7 +1158,7 @@ impl<W: LayoutElement> Monitor<W> {
// rendering for maximized GTK windows.
//
// FIXME: use proper bounds after fixing the Crop element.
let crop_bounds = if self.workspace_switch.is_some() {
let crop_bounds = if self.workspace_switch.is_some() || self.overview_progress.is_some() {
Rectangle::new(
Point::from((-i32::MAX / 2, 0)),
Size::from((i32::MAX, height)),
@@ -946,38 +1170,102 @@ impl<W: LayoutElement> Monitor<W> {
)
};
self.workspaces_with_render_geo()
.flat_map(move |(ws, geo)| {
ws.render_elements(renderer, target, focus_ring)
.filter_map(move |elem| {
CropRenderElement::from_element(elem, scale, crop_bounds)
})
.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,
)
})
})
let ws_scale = self.workspace_scale();
let overview_clamped_progress = self.overview_progress.as_ref().map(|p| p.clamped_value());
let is_overview_open = self.overview_open;
self.workspaces_with_render_geo().map(move |(ws, geo)| {
let iter = ws
.render_elements(renderer, target, focus_ring, is_overview_open)
.filter_map(move |elem| CropRenderElement::from_element(elem, scale, crop_bounds))
// .map(move |elem| {
// let elem_scale = 1. - (1. - ws_scale) / OVERVIEW_WORKSPACE_SCALE * 0.03;
// RescaleRenderElement::from_element(
// elem,
// size.downscale(2.)
// .to_physical_precise_round(scale)
// .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) {
let center_idx = self.active_workspace_idx;
let current_idx = self
.workspace_switch
.as_ref()
.map(|s| s.current_idx())
.unwrap_or(center_idx as f64);
let current_idx = self.workspace_render_idx();
let gesture = WorkspaceSwitchGesture {
center_idx,
current_idx,
tracker: SwipeTracker::new(),
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));
}
@@ -988,27 +1276,46 @@ impl<W: LayoutElement> Monitor<W> {
timestamp: Duration,
is_touchpad: bool,
) -> Option<bool> {
let ws_scale = self.workspace_scale().max(0.0001);
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return None;
};
if gesture.is_touchpad != is_touchpad {
if gesture.is_touchpad != is_touchpad || gesture.dnd_last_event_time.is_some() {
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);
let total_height = if gesture.is_touchpad {
WORKSPACE_GESTURE_MOVEMENT
} 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 min = gesture.center_idx.saturating_sub(1) as f64;
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
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 = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
let new_idx = rubber_band.clamp(min, max, new_idx);
if gesture.current_idx == new_idx {
return Some(false);
@@ -1018,11 +1325,102 @@ impl<W: LayoutElement> Monitor<W> {
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(
&mut self,
cancelled: bool,
is_touchpad: Option<bool>,
) -> bool {
let ws_scale = self.workspace_scale().max(0.0001);
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return false;
};
@@ -1041,28 +1439,35 @@ impl<W: LayoutElement> Monitor<W> {
let now = self.clock.now_unadjusted();
gesture.tracker.push(0., now);
let mut rubber_band = WORKSPACE_GESTURE_RUBBER_BAND;
rubber_band.limit /= ws_scale;
let total_height = if gesture.is_touchpad {
WORKSPACE_GESTURE_MOVEMENT
} else if gesture.dnd_last_event_time.is_some() {
WORKSPACE_DND_EDGE_SCROLL_MOVEMENT
} 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 current_pos = gesture.tracker.pos() / total_height;
let pos = gesture.tracker.projected_end_pos() / total_height;
let min = gesture.center_idx.saturating_sub(1) as f64;
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
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 = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
let new_idx = new_idx.clamp(min, max);
let new_idx = new_idx.round() as usize;
velocity *= WORKSPACE_GESTURE_RUBBER_BAND.clamp_derivative(
min,
max,
gesture.center_idx as f64 + current_pos,
);
velocity *= rubber_band.clamp_derivative(min, max, gesture.center_idx as f64 + current_pos);
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
@@ -1077,4 +1482,19 @@ impl<W: LayoutElement> Monitor<W> {
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);
}
}

View File

@@ -371,7 +371,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|| !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) {
let view_pos = Point::from((self.view_pos(), 0.));
let view_size = self.view_size;
let active_idx = self.active_column_idx;
@@ -384,7 +384,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
}
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);
self.insert_hint_element.update_render_elements(
area.size,
@@ -2274,7 +2274,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 {
InsertPosition::NewColumn(column_index) => {
if column_index == 0 || column_index == self.columns.len() {
@@ -2369,7 +2373,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
let view_size = self.view_size;
// 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.min(view_size.w - hint_area.size.w / 2.);
}
@@ -2724,6 +2728,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
renderer: &mut R,
target: RenderTarget,
focus_ring: bool,
is_overview_open: bool,
) -> Vec<ScrollingSpaceRenderElement<R>> {
let mut rv = vec![];
@@ -2731,7 +2736,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
// Draw the 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(
self.insert_hint_element
.render(renderer, area.loc)
@@ -2916,14 +2921,14 @@ impl<W: LayoutElement> ScrollingSpace<W> {
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 {
return;
return false;
};
let Some(last_time) = gesture.dnd_last_event_time else {
// Not a DnD scroll.
return;
return false;
};
let config = &self.options.gestures.dnd_edge_view_scroll;
@@ -2934,7 +2939,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
if delta == 0. {
// We're outside the scrolling zone.
gesture.dnd_nonzero_start_time = None;
return;
return false;
}
let nonzero_start = *gesture.dnd_nonzero_start_time.get_or_insert(now);
@@ -2943,7 +2948,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
// monitors.
let delay = Duration::from_millis(u64::from(config.delay_ms));
if now.saturating_sub(nonzero_start) < delay {
return;
return true;
}
let time_delta = now.saturating_sub(last_time).as_secs_f64();
@@ -2987,6 +2992,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
gesture.delta_from_tracker += clamped_offset - view_offset;
gesture.current_view_offset = clamped_offset;
true
}
pub fn view_offset_gesture_end(&mut self, _cancelled: bool, is_touchpad: Option<bool>) -> bool {

View File

@@ -576,6 +576,8 @@ enum Op {
ViewOffsetGestureBegin {
#[proptest(strategy = "1..=5usize")]
output_idx: usize,
#[proptest(strategy = "proptest::option::of(0..=4usize)")]
workspace_idx: Option<usize>,
is_touchpad: bool,
},
ViewOffsetGestureUpdate {
@@ -602,6 +604,13 @@ enum Op {
cancelled: bool,
is_touchpad: Option<bool>,
},
OverviewGestureBegin,
OverviewGestureUpdate {
#[proptest(strategy = "-400f64..400f64")]
delta: f64,
timestamp: Duration,
},
OverviewGestureEnd,
InteractiveMoveBegin {
#[proptest(strategy = "1..=5usize")]
window: usize,
@@ -657,6 +666,7 @@ enum Op {
#[proptest(strategy = "1..=5usize")]
window: usize,
},
ToggleOverview,
}
impl Op {
@@ -1345,6 +1355,7 @@ impl Op {
}
Op::ViewOffsetGestureBegin {
output_idx: id,
workspace_idx,
is_touchpad: normalize,
} => {
let name = format!("output{id}");
@@ -1352,7 +1363,7 @@ impl Op {
return;
};
layout.view_offset_gesture_begin(&output, normalize);
layout.view_offset_gesture_begin(&output, workspace_idx, normalize);
}
Op::ViewOffsetGestureUpdate {
delta,
@@ -1389,6 +1400,15 @@ impl Op {
} => {
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 {
window,
output_idx,
@@ -1442,6 +1462,9 @@ impl Op {
Op::InteractiveResizeEnd { 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::ViewOffsetGestureBegin {
output_idx: 1,
workspace_idx: None,
is_touchpad: true,
},
Op::ViewOffsetGestureEnd {

View File

@@ -2,7 +2,10 @@ use std::cmp::max;
use std::rc::Rc;
use std::time::Duration;
use niri_config::{CenterFocusedColumn, OutputName, PresetSize, Workspace as WorkspaceConfig};
use niri_config::{
CenterFocusedColumn, CornerRadius, FloatOrInt, OutputName, PresetSize,
Workspace as WorkspaceConfig,
};
use niri_ipc::{ColumnDisplay, PositionChange, SizeChange};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::{layer_map_for_output, Window};
@@ -18,6 +21,7 @@ use super::scrolling::{
Column, ColumnWidth, InsertHint, InsertPosition, ScrollDirection, ScrollingSpace,
ScrollingSpaceRenderElement,
};
use super::shadow::Shadow;
use super::tile::{Tile, TileRenderSnapshot};
use super::{
ActivateWindow, HitType, InteractiveResizeData, LayoutElement, Options, RemovedTile, SizeFrac,
@@ -25,6 +29,7 @@ use super::{
use crate::animation::Clock;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::RenderTarget;
use crate::utils::id::IdCounter;
use crate::utils::transaction::{Transaction, TransactionBlocker};
@@ -80,6 +85,9 @@ pub struct Workspace<W: LayoutElement> {
/// zones.
working_area: Rectangle<f64, Logical>,
/// This workspace's shadow in the overview.
shadow: Shadow,
/// Clock for driving animations.
pub(super) clock: Clock,
@@ -228,6 +236,17 @@ impl<W: LayoutElement> Workspace<W> {
options.clone(),
);
let shadow_config = niri_config::Shadow {
on: true,
offset: niri_config::ShadowOffset {
x: FloatOrInt(0.),
y: FloatOrInt(20.),
},
softness: FloatOrInt(120.),
spread: FloatOrInt(20.),
..Default::default()
};
Self {
scrolling,
floating,
@@ -237,6 +256,7 @@ impl<W: LayoutElement> Workspace<W> {
transform: output.current_transform(),
view_size,
working_area,
shadow: Shadow::new(shadow_config),
output: Some(output),
clock,
base_options,
@@ -281,6 +301,17 @@ impl<W: LayoutElement> Workspace<W> {
options.clone(),
);
let shadow_config = niri_config::Shadow {
on: true,
offset: niri_config::ShadowOffset {
x: FloatOrInt(0.),
y: FloatOrInt(20.),
},
softness: FloatOrInt(120.),
spread: FloatOrInt(20.),
..Default::default()
};
Self {
scrolling,
floating,
@@ -291,6 +322,7 @@ impl<W: LayoutElement> Workspace<W> {
original_output,
view_size,
working_area,
shadow: Shadow::new(shadow_config),
clock,
base_options,
options,
@@ -336,13 +368,23 @@ impl<W: LayoutElement> Workspace<W> {
self.scrolling.are_transitions_ongoing() || self.floating.are_transitions_ongoing()
}
pub fn update_render_elements(&mut self, is_active: bool) {
self.scrolling
.update_render_elements(is_active && !self.floating_is_active.get());
pub fn update_render_elements(&mut self, is_active: bool, is_overview_open: bool) {
self.scrolling.update_render_elements(
is_active && !self.floating_is_active.get(),
is_overview_open,
);
let view_rect = Rectangle::from_size(self.view_size);
self.floating
.update_render_elements(is_active && self.floating_is_active.get(), view_rect);
self.shadow.update_render_elements(
self.view_size,
true,
CornerRadius::default(),
self.scale.fractional_scale(),
1.,
);
}
pub fn update_config(&mut self, base_options: Rc<Options>) {
@@ -370,6 +412,7 @@ impl<W: LayoutElement> Workspace<W> {
pub fn update_shaders(&mut self) {
self.scrolling.update_shaders();
self.floating.update_shaders();
self.shadow.update_shaders();
}
pub fn windows(&self) -> impl Iterator<Item = &W> + '_ {
@@ -1409,11 +1452,15 @@ impl<W: LayoutElement> Workspace<W> {
renderer: &mut R,
target: RenderTarget,
focus_ring: bool,
is_overview_open: bool,
) -> impl Iterator<Item = WorkspaceRenderElement<R>> {
let scrolling_focus_ring = focus_ring && !self.floating_is_active();
let scrolling = self
.scrolling
.render_elements(renderer, target, scrolling_focus_ring);
let scrolling = self.scrolling.render_elements(
renderer,
target,
scrolling_focus_ring,
is_overview_open,
);
let scrolling = scrolling.into_iter().map(WorkspaceRenderElement::from);
let floating_focus_ring = focus_ring && self.floating_is_active();
@@ -1428,6 +1475,13 @@ impl<W: LayoutElement> Workspace<W> {
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 {
self.scrolling.render_above_top_layer()
}
@@ -1628,7 +1682,7 @@ impl<W: LayoutElement> Workspace<W> {
self.scrolling.dnd_scroll_gesture_begin();
}
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>) {
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>, speed: f64) -> bool {
let config = &self.options.gestures.dnd_edge_view_scroll;
let trigger_width = config.trigger_width.0;
@@ -1654,8 +1708,9 @@ impl<W: LayoutElement> Workspace<W> {
// Normalize to [0, 1].
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) {

View File

@@ -15,7 +15,7 @@ use anyhow::{bail, ensure, Context};
use calloop::futures::Scheduler;
use niri_config::{
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::input::Keycode;
@@ -26,7 +26,8 @@ use smithay::backend::renderer::element::surface::{
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
};
use smithay::backend::renderer::element::utils::{
select_dmabuf_feedback, Relocate, RelocateRenderElement,
select_dmabuf_feedback, CropRenderElement, Relocate, RelocateRenderElement,
RescaleRenderElement,
};
use smithay::backend::renderer::element::{
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::MappedLayer;
use crate::layout::tile::TileRenderElement;
use crate::layout::workspace::WorkspaceId;
use crate::layout::workspace::{Workspace, WorkspaceId};
use crate::layout::{HitType, Layout, LayoutElement as _, MonitorRenderElement};
use crate::niri_render_elements;
use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
@@ -344,6 +345,7 @@ pub struct Niri {
/// Used for limiting the notify to once per iteration, so that it's not spammed with high
/// resolution mice.
pub notified_activity_this_iteration: bool,
pub pointer_inside_hot_corner: bool,
pub tablet_cursor_location: Option<Point<f64, Logical>>,
pub gesture_swipe_3f_cumulative: Option<(f64, f64)>,
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
/// tracking issues and make screenshots easier.
pub background_buffer: SolidColorBuffer,
pub backdrop_buffer: SolidColorBuffer,
pub lock_render_state: LockRenderState,
pub lock_surface: Option<LockSurface>,
pub lock_color_buffer: SolidColorBuffer,
@@ -467,6 +470,7 @@ pub enum KeyboardFocus {
LayerShell { surface: WlSurface },
LockScreen { surface: Option<WlSurface> },
ScreenshotUi,
Overview,
}
#[derive(Default, Clone, PartialEq)]
@@ -563,6 +567,7 @@ impl KeyboardFocus {
KeyboardFocus::LayerShell { surface } => Some(surface),
KeyboardFocus::LockScreen { surface } => surface.as_ref(),
KeyboardFocus::ScreenshotUi => None,
KeyboardFocus::Overview => None,
}
}
@@ -572,12 +577,17 @@ impl KeyboardFocus {
KeyboardFocus::LayerShell { surface } => Some(surface),
KeyboardFocus::LockScreen { surface } => surface,
KeyboardFocus::ScreenshotUi => None,
KeyboardFocus::Overview => None,
}
}
pub fn is_layout(&self) -> bool {
matches!(self, KeyboardFocus::Layout { .. })
}
pub fn is_overview(&self) -> bool {
matches!(self, KeyboardFocus::Overview)
}
}
pub struct State {
@@ -1040,6 +1050,11 @@ impl State {
surface = surface.or_else(|| focus_on_layer(Layer::Background));
} else {
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::Background));
surface = surface.or_else(layout_focus);
@@ -2392,6 +2407,7 @@ impl Niri {
pointer_inactivity_timer: None,
pointer_inactivity_timer_got_reset: false,
notified_activity_this_iteration: false,
pointer_inside_hot_corner: false,
tablet_cursor_location: None,
gesture_swipe_3f_cumulative: None,
vertical_wheel_tracker: ScrollTracker::new(120),
@@ -2640,6 +2656,9 @@ impl Niri {
.to_array_unpremul();
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.
if name.connector == "winit" {
transform = Transform::Flipped180;
@@ -2673,6 +2692,7 @@ impl Niri {
last_drm_sequence: None,
frame_callback_sequence: 0,
background_buffer: SolidColorBuffer::new(size, background_color),
backdrop_buffer: SolidColorBuffer::new(size, backdrop_color),
lock_render_state,
lock_surface: None,
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) {
state.background_buffer.resize(output_size);
state.backdrop_buffer.resize(output_size);
state.lock_color_buffer.resize(output_size);
if let Some(lock_surface) = &state.lock_surface {
@@ -2876,17 +2897,58 @@ impl Niri {
return false;
}
if layer_popup_under(Layer::Top)
|| layer_toplevel_under(Layer::Top)
|| layer_popup_under(Layer::Bottom)
|| layer_popup_under(Layer::Background)
{
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
@@ -3017,6 +3079,8 @@ impl Niri {
let mut under =
layer_popup_under(Layer::Overlay).or_else(|| layer_toplevel_under(Layer::Overlay));
let is_overview_open = self.layout.is_overview_open();
// When rendering above the top layer, we put the regular monitor elements first.
// Otherwise, we will render all layer-shell pop-ups and the top layer on top.
if mon.render_above_top_layer() {
@@ -3029,14 +3093,28 @@ impl Niri {
.or_else(|| layer_toplevel_under(Layer::Bottom))
.or_else(|| layer_toplevel_under(Layer::Background));
} else {
let hot_corner = Rectangle::from_size(Size::from((1., 1.)));
if hot_corner.contains(pos_within_output) {
return rv;
}
under = under
.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::Background))
.or_else(window_under)
.or_else(|| layer_toplevel_under(Layer::Bottom))
.or_else(|| layer_toplevel_under(Layer::Background));
.or_else(|| layer_toplevel_under(Layer::Top));
if !is_overview_open {
under = under
.or_else(|| layer_popup_under(Layer::Bottom))
.or_else(|| layer_popup_under(Layer::Background));
}
under = under.or_else(window_under);
if !is_overview_open {
under = under
.or_else(|| layer_toplevel_under(Layer::Bottom))
.or_else(|| layer_toplevel_under(Layer::Background));
}
}
let Some((mut surface_and_pos, (window, layer))) = under else {
@@ -3495,6 +3573,7 @@ impl Niri {
// layer-shell, the layout will briefly draw as active, despite never having focus.
KeyboardFocus::LockScreen { .. } => true,
KeyboardFocus::ScreenshotUi => true,
KeyboardFocus::Overview => true,
};
self.layout.refresh(layout_is_active);
@@ -3748,10 +3827,18 @@ impl Niri {
return elements;
}
// Prepare the background element.
// Prepare the background elements.
let state = self.output_state.get(output).unwrap();
let background_buffer = state.background_buffer.clone();
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),
output_scale,
1.,
@@ -3768,8 +3855,8 @@ impl Niri {
.map(OutputRenderElements::from),
);
// Add the background for outputs that were connected while the screenshot UI was open.
elements.push(background);
// Add the backdrop for outputs that were connected while the screenshot UI was open.
elements.push(backdrop);
if self.debug_draw_opaque_regions {
draw_opaque_regions(&mut elements, output_scale);
@@ -3788,7 +3875,11 @@ impl Niri {
// Get monitor elements.
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
.layout
.render_interactive_move_for_output(renderer, output, target)
@@ -3824,27 +3915,71 @@ impl Niri {
.into_iter()
.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.normal.drain(..).map(OutputRenderElements::from));
// TODO background
} else {
elements.extend(top_layer.into_iter().map(OutputRenderElements::from));
elements.extend(layer_elems.popups.drain(..).map(OutputRenderElements::from));
// TODO: adjust input to put interactive move above popups.
elements.extend(
int_move_elements
.into_iter()
.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.
elements.push(background);
// Then the backdrop.
elements.push(backdrop);
if self.debug_draw_opaque_regions {
draw_opaque_regions(&mut elements, output_scale);
@@ -5433,7 +5568,7 @@ impl Niri {
}
if let Some(window) = &new_focus.window {
if current_focus.window.as_ref() != Some(window) {
if !self.layout.is_overview_open() && current_focus.window.as_ref() != Some(window) {
let (window, hit) = window;
// Don't trigger focus-follows-mouse over the tab indicator.
@@ -5671,10 +5806,17 @@ niri_render_elements! {
OutputRenderElements<R> => {
Monitor = MonitorRenderElement<R>,
Tile = TileRenderElement<R>,
RescaledTile = RescaleRenderElement<TileRenderElement<R>>,
LayerSurface = LayerSurfaceRenderElement<R>,
RelocatedLayerSurface = CropRenderElement<RelocateRenderElement<RescaleRenderElement<
LayerSurfaceRenderElement<R>
>>>,
Wayland = WaylandSurfaceRenderElement<R>,
NamedPointer = MemoryRenderBufferRenderElement<R>,
SolidColor = SolidColorRenderElement,
RelocatedSolidColor = CropRenderElement<RelocateRenderElement<RescaleRenderElement<
SolidColorRenderElement
>>>,
ScreenshotUi = ScreenshotUiRenderElement,
Texture = PrimaryGpuTextureRenderElement,
// Used for the CPU-rendered panels.

View File

@@ -245,6 +245,11 @@ impl ShaderRenderElement {
self.area.loc = location;
self
}
pub fn with_alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha;
self
}
}
impl Element for ShaderRenderElement {

View File

@@ -175,6 +175,11 @@ impl ShadowRenderElement {
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 {
Shaders::get(renderer)
.program(ProgramType::Shadow)