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::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(
[
+2
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.
+1
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()
}
+1
View File
@@ -115,6 +115,7 @@ impl TestCase for Tile {
self.tile.update_render_elements(
true,
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
1.,
);
self.tile
.render(renderer, location, true, RenderTarget::Output)
+17 -1
View File
@@ -153,7 +153,7 @@ impl XdgShellHandler for State {
match start_data {
PointerOrTouchStartData::Pointer(start_data) => {
let grab = MoveGrab::new(start_data, window);
let grab = MoveGrab::new(start_data, window, false);
pointer.set_grab(self, grab, serial, Focus::Clear);
}
PointerOrTouchStartData::Touch(start_data) => {
@@ -316,6 +316,9 @@ impl XdgShellHandler for State {
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
// FIXME: somewhere here we probably need to check is_overview_open to match the logic
// in update_keyboard_focus().
if layers
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
.is_none()
@@ -1062,6 +1065,19 @@ impl State {
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
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 -= get_popup_toplevel_coords(popup);
+241 -26
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;
+42 -5
View File
@@ -1,10 +1,11 @@
use smithay::backend::input::ButtonState;
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
RelativeMotionEvent,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
@@ -15,14 +16,32 @@ pub struct MoveGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
gesture: GestureState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GestureState {
Recognizing,
Move,
}
impl MoveGrab {
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
pub fn new(
start_data: PointerGrabStartData<State>,
window: Window,
use_threshold: bool,
) -> Self {
let gesture = if use_threshold {
GestureState::Recognizing
} else {
GestureState::Move
};
Self {
last_location: start_data.location,
start_data,
window,
gesture,
}
}
@@ -53,6 +72,24 @@ impl PointerGrab<State> for MoveGrab {
let output = output.clone();
let event_delta = event.location - self.last_location;
self.last_location = event.location;
if self.gesture == GestureState::Recognizing {
let c = event.location - self.start_data.location;
// Check if the gesture moved far enough to decide.
if c.x * c.x + c.y * c.y >= 8. * 8. {
self.gesture = GestureState::Move;
data.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
}
}
if self.gesture != GestureState::Move {
return;
}
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,
+26 -4
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);
+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()
}
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();
for (tile, offset) in self.tiles_with_offsets_mut() {
let id = tile.window().id();
@@ -271,7 +276,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
let mut tile_view_rect = view_rect;
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::renderer::NiriRenderer;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::utils::round_logical_in_physical_max1;
#[derive(Debug)]
pub struct FocusRing {
@@ -64,7 +65,9 @@ impl FocusRing {
scale: f64,
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.);
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::iter::zip;
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};
@@ -13,10 +14,12 @@ 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;
@@ -30,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.
@@ -44,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.
@@ -65,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.
@@ -84,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 {
@@ -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> {
pub fn new(
output: Output,
@@ -138,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,
@@ -211,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());
@@ -232,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),
)));
}
@@ -638,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 {
@@ -666,31 +751,10 @@ impl<W: LayoutElement> Monitor<W> {
}
pub fn update_render_elements(&mut self, is_active: bool) {
match &self.workspace_switch {
Some(switch) => {
let render_idx = switch.current_idx();
let before_idx = render_idx.floor();
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);
}
let is_overview_open = self.overview_open;
let extra_overview_scale = self.workspace_scale();
for (ws, _) in self.workspaces_with_render_geo_mut() {
ws.update_render_elements(is_active, is_overview_open, extra_overview_scale);
}
}
@@ -826,6 +890,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 {
@@ -841,77 +909,221 @@ impl<W: LayoutElement> Monitor<W> {
Some(rect)
}
pub fn workspaces_with_render_positions(
&self,
) -> impl Iterator<Item = (&Workspace<W>, Point<f64, Logical>)> {
let mut first = None;
let mut second = None;
pub fn workspace_scale(&self) -> f64 {
if let Some(p) = &self.overview_progress {
(1. - p.value() * (1. - OVERVIEW_WORKSPACE_SCALE)).max(0.)
} else {
1.
}
}
match &self.workspace_switch {
Some(switch) => {
let render_idx = switch.current_idx();
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
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 after_idx >= 0. && before_idx < self.workspaces.len() as f64 {
let scale = self.output.current_scale().fractional_scale();
let size = output_size(&self.output);
let offset =
round_logical_in_physical(scale, (render_idx - before_idx) * size.h);
// Ceil the height in physical pixels.
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.)),
));
// 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.);
}
}
}
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(
&self,
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 (ws, bounds) = self
.workspaces_with_render_positions()
.map(|(ws, offset)| (ws, Rectangle::new(offset, size)))
.find(|(_, bounds)| bounds.contains(pos_within_output))?;
Some((ws, bounds.loc))
let (ws, geo) = self.workspaces_with_render_geo().find_map(|(ws, geo)| {
// Extend width to entire output.
let loc = Point::from((0., geo.loc.y));
let size = Size::from((size.w, geo.size.h));
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)> {
let (ws, offset) = self.workspace_under(pos_within_output)?;
let (win, hit) = ws.window_under(pos_within_output - offset)?;
Some((win, hit.offset_win_pos(offset)))
let (ws, geo) = self.workspace_under(pos_within_output)?;
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> {
let (ws, offset) = self.workspace_under(pos_within_output)?;
ws.resize_edges_under(pos_within_output - offset)
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;
}
@@ -924,7 +1136,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();
@@ -942,7 +1159,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)),
@@ -954,38 +1171,102 @@ impl<W: LayoutElement> Monitor<W> {
)
};
self.workspaces_with_render_positions()
.flat_map(move |(ws, offset)| {
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.
offset.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));
}
@@ -996,27 +1277,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);
@@ -1026,11 +1326,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;
};
@@ -1049,28 +1440,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());
@@ -1085,4 +1483,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);
}
}
+77 -21
View File
@@ -21,7 +21,7 @@ use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::ResizeEdge;
use crate::utils::{round_logical_in_physical_max1, ResizeEdge};
use crate::window::ResolvedWindowRules;
/// 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: 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: Clock,
@@ -288,6 +293,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
view_size,
working_area,
scale,
extra_overview_scale: 1.,
clock,
options,
}
@@ -371,7 +377,14 @@ 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,
extra_overview_scale: f64,
) {
self.extra_overview_scale = extra_overview_scale;
let view_pos = Point::from((self.view_pos(), 0.));
let view_size = self.view_size;
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_pos = view_pos - col_off - col.render_offset();
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(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,
@@ -2183,14 +2196,41 @@ impl<W: LayoutElement> ScrollingSpace<W> {
.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(
&self,
data: impl Iterator<Item = ColumnData>,
) -> impl Iterator<Item = f64> {
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
.column_xs(data)
.visual_column_xs(data)
.enumerate()
.filter_map(move |(idx, pos)| (idx != active_idx).then_some(pos));
iter::once(active_pos).chain(offsets)
@@ -2234,7 +2274,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
pub fn tiles_with_render_positions(
&self,
) -> 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.));
self.columns_in_render_order()
.flat_map(move |(col, col_x)| {
@@ -2255,7 +2295,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
&mut self,
round: bool,
) -> 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.));
self.columns_in_render_order_mut()
.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 {
InsertPosition::NewColumn(column_index) => {
if column_index == 0 || column_index == self.columns.len() {
@@ -2369,7 +2413,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,14 +2768,16 @@ impl<W: LayoutElement> ScrollingSpace<W> {
renderer: &mut R,
target: RenderTarget,
focus_ring: bool,
is_overview_open: bool,
) -> Vec<ScrollingSpaceRenderElement<R>> {
let mut rv = vec![];
let scale = Scale::from(self.scale);
let visual_scale = scale * self.extra_overview_scale;
// 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)
@@ -2762,7 +2808,9 @@ impl<W: LayoutElement> ScrollingSpace<W> {
// Draw the tab indicator on top.
{
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));
}
@@ -2770,7 +2818,9 @@ impl<W: LayoutElement> ScrollingSpace<W> {
let tile_pos =
view_off + col_off + col_render_off + tile_off + tile.render_offset();
// 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.
@@ -2916,14 +2966,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 +2984,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 +2993,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 +3037,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 {
@@ -3816,14 +3867,19 @@ impl<W: LayoutElement> Column<W> {
|| 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;
for (tile_idx, (tile, tile_off)) in self.tiles_mut().enumerate() {
let is_active = is_active && tile_idx == active_idx;
let mut tile_view_rect = view_rect;
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();
@@ -3849,7 +3905,7 @@ impl<W: LayoutElement> Column<W> {
self.tiles.len(),
tabs,
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::render_helpers::border::BorderRenderElement;
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)]
pub struct TabIndicator {
@@ -77,12 +79,13 @@ impl TabIndicator {
scale: f64,
) -> impl Iterator<Item = Rectangle<f64, 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 width = round(self.config.width.0);
let gap = round(self.config.gap.0);
let gaps_between = round(self.config.gaps_between_tabs.0);
let width = round_max1(self.config.width.0);
let gap = round_max1(self.config.gap.0);
let gaps_between = round_max1(self.config.gaps_between_tabs.0);
let position = self.config.position;
let side = match position {
+25 -1
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 {
+73 -11
View File
@@ -25,8 +25,8 @@ use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::RenderTarget;
use crate::utils::round_logical_in_physical;
use crate::utils::transaction::Transaction;
use crate::utils::{round_logical_in_physical, round_logical_in_physical_max1};
/// Toplevel window with decorations.
#[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: 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.
pub(super) clock: Clock,
@@ -193,6 +198,7 @@ impl<W: LayoutElement> Tile<W> {
rounded_corner_damage: Default::default(),
view_size,
scale,
extra_overview_scale: 1.,
clock,
options,
}
@@ -346,9 +352,17 @@ impl<W: LayoutElement> Tile<W> {
.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();
self.extra_overview_scale = extra_overview_scale;
let visual_scale = self.scale * extra_overview_scale;
let draw_border_with_background = rules
.draw_border_with_background
.unwrap_or_else(|| !self.window.has_ssd());
@@ -371,7 +385,7 @@ impl<W: LayoutElement> Tile<W> {
view_rect.size,
),
radius,
self.scale,
visual_scale,
1.,
);
@@ -386,7 +400,7 @@ impl<W: LayoutElement> Tile<W> {
self.animated_tile_size(),
is_active,
radius,
self.scale,
visual_scale,
1.,
);
@@ -402,7 +416,7 @@ impl<W: LayoutElement> Tile<W> {
!draw_focus_ring_with_background,
view_rect,
radius,
self.scale,
visual_scale,
1.,
);
}
@@ -546,6 +560,12 @@ impl<W: LayoutElement> Tile<W> {
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.
pub fn window_loc(&self) -> Point<f64, Logical> {
let mut loc = Point::from((0., 0.));
@@ -577,6 +597,38 @@ impl<W: LayoutElement> Tile<W> {
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> {
let mut size = self.window_size();
@@ -635,6 +687,8 @@ impl<W: LayoutElement> Tile<W> {
pub fn animated_window_size(&self) -> Size<f64, Logical> {
let mut size = self.window_size();
let visual_scale = self.scale * self.extra_overview_scale;
if let Some(resize) = &self.resize_animation {
let val = resize.anim.value();
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.h = f64::max(1., size_from.h + (size.h - size_from.h) * val);
size = size
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
.to_physical_precise_round(visual_scale)
.to_logical(visual_scale);
}
size
@@ -652,15 +706,20 @@ impl<W: LayoutElement> Tile<W> {
pub fn animated_tile_size(&self) -> Size<f64, Logical> {
let mut size = self.animated_window_size();
let visual_scale = self.scale * self.extra_overview_scale;
if self.is_fullscreen {
// 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.
size.w = f64::max(size.w, self.view_size.w);
size.h = f64::max(size.h, self.view_size.h);
size = size
.to_physical_precise_round(visual_scale)
.to_logical(visual_scale);
return size;
}
if let Some(width) = self.effective_border_width() {
if let Some(width) = self.visual_effective_border_width() {
size.w += width * 2.;
size.h += width * 2.;
}
@@ -798,10 +857,12 @@ impl<W: LayoutElement> Tile<W> {
return Point::from((0., 0.));
}
let visual_scale = self.scale * self.extra_overview_scale;
let now = self.clock.now().as_secs_f64();
let amplitude = self.view_size.h / 96.;
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))
}
@@ -826,6 +887,7 @@ impl<W: LayoutElement> Tile<W> {
let _span = tracy_client::span!("Tile::render_inner");
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() {
1.
@@ -843,7 +905,7 @@ impl<W: LayoutElement> Tile<W> {
// passed to update_render_elements(). But, it works well enough for what it is.
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 animated_window_size = self.animated_window_size();
let window_render_loc = location + window_loc;
@@ -1035,7 +1097,7 @@ impl<W: LayoutElement> Tile<W> {
});
let rv = rv.chain(elem);
let elem = self.effective_border_width().map(|width| {
let elem = self.visual_effective_border_width().map(|width| {
self.border
.render(renderer, location + Point::from((width, width)))
.map(Into::into)
+78 -12
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,34 @@ 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,
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);
self.floating
.update_render_elements(is_active && self.floating_is_active.get(), view_rect);
self.floating.update_render_elements(
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>) {
@@ -370,6 +423,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 +1463,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 +1486,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()
}
@@ -1446,7 +1511,7 @@ impl<W: LayoutElement> Workspace<W> {
if tile.window().id() == window {
let view_pos = Point::from((-tile_pos.x, -tile_pos.y));
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);
return;
}
@@ -1628,7 +1693,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 +1719,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) {
+205 -45
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 {
@@ -2837,17 +2858,11 @@ impl Niri {
Some((output, pos_within_output))
}
/// 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)?;
pub fn is_layout_obscured_under(
&self,
output: &Output,
pos_within_output: Point<f64, Logical>,
) -> bool {
// The ordering here must be consistent with the ordering in render() so that input is
// consistent with the visuals.
@@ -2874,16 +2889,78 @@ impl Niri {
let layer_popup_under = |layer| layer_surface_under(layer, true);
if layer_popup_under(Layer::Overlay) || layer_toplevel_under(Layer::Overlay) {
return None;
return true;
}
let mon = self.layout.monitor_for_output(output).unwrap();
if !mon.render_above_top_layer()
&& (layer_popup_under(Layer::Top)
|| layer_popup_under(Layer::Bottom)
|| layer_popup_under(Layer::Background)
|| layer_toplevel_under(Layer::Top))
{
if mon.render_above_top_layer() {
return false;
}
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;
}
@@ -3002,26 +3079,42 @@ 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() {
under = under
.or_else(window_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(|| layer_toplevel_under(Layer::Top))
.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_popup_under(Layer::Bottom))
.or_else(|| layer_popup_under(Layer::Background))
.or_else(|| layer_toplevel_under(Layer::Top))
.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 {
@@ -3480,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);
@@ -3733,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.,
@@ -3753,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);
@@ -3773,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)
@@ -3791,10 +3897,13 @@ impl Niri {
extend_from_layer(&mut layer_elems, Layer::Overlay);
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();
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::Background);
@@ -3806,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(top_layer_normal.into_iter().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(
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);
@@ -5415,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.
@@ -5653,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.
+5
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 {
+5
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)