aboutsummaryrefslogtreecommitdiff
path: root/src/layout/monitor.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/layout/monitor.rs')
-rw-r--r--src/layout/monitor.rs236
1 files changed, 216 insertions, 20 deletions
diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs
index ab5384a8..54f8820f 100644
--- a/src/layout/monitor.rs
+++ b/src/layout/monitor.rs
@@ -5,7 +5,7 @@ use std::time::Duration;
use niri_config::CornerRadius;
use smithay::backend::renderer::element::utils::{
- CropRenderElement, Relocate, RelocateRenderElement,
+ CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement,
};
use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Size};
@@ -17,11 +17,12 @@ use super::workspace::{
compute_working_area, OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId,
WorkspaceRenderElement,
};
-use super::{ActivateWindow, HitType, LayoutElement, Options};
+use super::{compute_overview_zoom, ActivateWindow, HitType, LayoutElement, Options};
use crate::animation::{Animation, Clock};
use crate::input::swipe_tracker::SwipeTracker;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
+use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::RenderTarget;
use crate::rubber_band::RubberBand;
use crate::utils::transaction::Transaction;
@@ -68,6 +69,10 @@ pub struct Monitor<W: LayoutElement> {
insert_hint_element: InsertHintElement,
/// Location to render the insert hint element.
insert_hint_render_loc: Option<InsertHintRenderLoc>,
+ /// Whether the overview is open.
+ pub(super) overview_open: bool,
+ /// Progress of the overview zoom animation, 1 is fully in overview.
+ overview_progress: Option<OverviewProgress>,
/// Clock for driving animations.
pub(super) clock: Clock,
/// Configurable properties of the layout.
@@ -94,6 +99,8 @@ pub struct WorkspaceSwitchGesture {
tracker: SwipeTracker,
/// Whether the gesture is controlled by the touchpad.
is_touchpad: bool,
+ /// Whether the gesture is clamped to +-1 workspace around the center.
+ is_clamped: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -116,6 +123,12 @@ struct InsertHintRenderLoc {
location: Point<f64, Logical>,
}
+#[derive(Debug)]
+pub(super) enum OverviewProgress {
+ Animation(Animation),
+ Value(f64),
+}
+
/// Where to put a newly added window.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum MonitorAddWindowTarget<'a, W: LayoutElement> {
@@ -137,10 +150,12 @@ niri_render_elements! {
MonitorInnerRenderElement<R> => {
Workspace = CropRenderElement<WorkspaceRenderElement<R>>,
InsertHint = CropRenderElement<InsertHintRenderElement>,
+ Shadow = ShadowRenderElement,
}
}
-pub type MonitorRenderElement<R> = RelocateRenderElement<MonitorInnerRenderElement<R>>;
+pub type MonitorRenderElement<R> =
+ RelocateRenderElement<RescaleRenderElement<MonitorInnerRenderElement<R>>>;
impl WorkspaceSwitch {
pub fn current_idx(&self) -> f64 {
@@ -183,9 +198,38 @@ impl WorkspaceSwitch {
impl WorkspaceSwitchGesture {
fn min_max(&self, workspace_count: usize) -> (f64, f64) {
- let min = self.center_idx.saturating_sub(1) as f64;
- let max = (self.center_idx + 1).min(workspace_count - 1) as f64;
- (min, max)
+ if self.is_clamped {
+ let min = self.center_idx.saturating_sub(1) as f64;
+ let max = (self.center_idx + 1).min(workspace_count - 1) as f64;
+ (min, max)
+ } else {
+ (0., (workspace_count - 1) as f64)
+ }
+ }
+}
+
+impl OverviewProgress {
+ pub fn value(&self) -> f64 {
+ match self {
+ OverviewProgress::Animation(anim) => anim.value(),
+ OverviewProgress::Value(v) => *v,
+ }
+ }
+
+ pub fn clamped_value(&self) -> f64 {
+ match self {
+ OverviewProgress::Animation(anim) => anim.clamped_value(),
+ OverviewProgress::Value(v) => *v,
+ }
+ }
+}
+
+impl From<&super::OverviewProgress> for OverviewProgress {
+ fn from(value: &super::OverviewProgress) -> Self {
+ match value {
+ super::OverviewProgress::Animation(anim) => Self::Animation(anim.clone()),
+ super::OverviewProgress::Gesture(gesture) => Self::Value(gesture.value),
+ }
}
}
@@ -212,6 +256,8 @@ impl<W: LayoutElement> Monitor<W> {
insert_hint: None,
insert_hint_element: InsertHintElement::new(options.insert_hint),
insert_hint_render_loc: None,
+ overview_open: false,
+ overview_progress: None,
workspace_switch: None,
clock,
options,
@@ -769,7 +815,9 @@ impl<W: LayoutElement> Monitor<W> {
// Make sure the hint is at least partially visible.
if matches!(hint.position, InsertPosition::NewColumn(_)) {
+ let zoom = self.overview_zoom();
let geo = insert_hint_ws_geo.unwrap();
+ let geo = geo.downscale(zoom);
area.loc.x = area.loc.x.max(-geo.loc.x - area.size.w / 2.);
area.loc.x = area.loc.x.min(geo.loc.x + geo.size.w - area.size.w / 2.);
@@ -942,6 +990,10 @@ impl<W: LayoutElement> Monitor<W> {
///
/// During animations, assumes the final view position.
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
+ if self.overview_open {
+ return None;
+ }
+
self.active_workspace_ref().active_tile_visual_rectangle()
}
@@ -962,7 +1014,100 @@ impl<W: LayoutElement> Monitor<W> {
self.workspace_size(zoom) + Size::from((0., gap))
}
+ pub fn overview_zoom(&self) -> f64 {
+ let progress = self.overview_progress.as_ref().map(|p| p.value());
+ compute_overview_zoom(&self.options, progress)
+ }
+
+ pub(super) fn set_overview_progress(&mut self, progress: Option<&super::OverviewProgress>) {
+ let prev_render_idx = self.workspace_render_idx();
+ self.overview_progress = progress.map(OverviewProgress::from);
+ let new_render_idx = self.workspace_render_idx();
+
+ // If the view jumped (can happen when going from corrected to uncorrected render_idx, for
+ // example when toggling the overview in the middle of an overview animation), then restart
+ // the workspace switch to avoid jumps.
+ if prev_render_idx != new_render_idx {
+ if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
+ // FIXME: maintain velocity.
+ *anim = anim.restarted(prev_render_idx, anim.to(), 0.);
+ }
+ }
+ }
+
+ #[cfg(test)]
+ pub(super) fn overview_progress_value(&self) -> Option<f64> {
+ self.overview_progress.as_ref().map(|p| p.value())
+ }
+
pub fn workspace_render_idx(&self) -> f64 {
+ // If workspace switch and overview progress are matching animations, then compute a
+ // correction term to make the movement appear monotonic.
+ if let (
+ Some(WorkspaceSwitch::Animation(switch_anim)),
+ Some(OverviewProgress::Animation(progress_anim)),
+ ) = (&self.workspace_switch, &self.overview_progress)
+ {
+ if switch_anim.start_time() == progress_anim.start_time()
+ && (switch_anim.duration().as_secs_f64() - progress_anim.duration().as_secs_f64())
+ .abs()
+ <= 0.001
+ {
+ #[rustfmt::skip]
+ // How this was derived:
+ //
+ // - Assume we're animating a zoom + switch. Consider switch "from" and "to".
+ // These are render_idx values, so first workspace to second would have switch
+ // from = 0. and to = 1. regardless of the zoom level.
+ //
+ // - At the start, the point at "from" is at Y = 0. We're moving the point at "to"
+ // to Y = 0. We want this to be a monotonic motion in apparent coordinates (after
+ // zoom).
+ //
+ // - Height at the start:
+ // from_height = (size.h + gap) * from_zoom.
+ //
+ // - Current height:
+ // current_height = (size.h + gap) * zoom.
+ //
+ // - We're moving the "to" point to Y = 0:
+ // to_y = 0.
+ //
+ // - The initial position of the point we're moving:
+ // from_y = (to - from) * from_height.
+ //
+ // - We want this point to travel monotonically in apparent coordinates:
+ // current_y = from_y + (to_y - from_y) * progress,
+ // where progress is from 0 to 1, equals to the animation progress (switch and
+ // zoom are the same since they are synchronized).
+ //
+ // - Derive the Y of the first workspace from this:
+ // first_y = current_y - to * current_height.
+ //
+ // Now, let's substitute and rearrange the terms.
+ //
+ // - current_y = from_y + (0 - (to - from) * from_height) * progress
+ // - progress = (switch_anim.value() - from) / (to - from)
+ // - current_y = from_y - (to - from) * from_height * (switch_anim.value() - from) / (to - from)
+ // - current_y = from_y - from_height * (switch_anim.value() - from)
+ // - first_y = from_y - from_height * (switch_anim.value() - from) - to * current_height
+ // - first_y = (to - from) * from_height - from_height * (switch_anim.value() - from) - to * current_height
+ // - first_y = to * from_height - switch_anim.value() * from_height - to * current_height
+ // - first_y = -switch_anim.value() * from_height + to * (from_height - current_height)
+ let from = progress_anim.from();
+ let from_zoom = compute_overview_zoom(&self.options, Some(from));
+ let from_ws_height_with_gap = self.workspace_size_with_gap(from_zoom).h;
+
+ let zoom = self.overview_zoom();
+ let ws_height_with_gap = self.workspace_size_with_gap(zoom).h;
+
+ let first_ws_y = -switch_anim.value() * from_ws_height_with_gap
+ + switch_anim.to() * (from_ws_height_with_gap - ws_height_with_gap);
+
+ return -first_ws_y / ws_height_with_gap;
+ }
+ };
+
if let Some(switch) = &self.workspace_switch {
switch.current_idx()
} else {
@@ -972,19 +1117,24 @@ impl<W: LayoutElement> Monitor<W> {
pub fn workspaces_render_geo(&self) -> impl Iterator<Item = Rectangle<f64, Logical>> {
let scale = self.scale.fractional_scale();
- let zoom = 1.;
+ let zoom = self.overview_zoom();
let ws_size = self.workspace_size(zoom);
let gap = self.workspace_gap(zoom);
let ws_height_with_gap = ws_size.h + gap;
+ let static_offset = (self.view_size.to_point() - ws_size.to_point()).downscale(2.);
+ let static_offset = static_offset
+ .to_physical_precise_round(scale)
+ .to_logical(scale);
+
let first_ws_y = -self.workspace_render_idx() * ws_height_with_gap;
let first_ws_y = round_logical_in_physical(scale, first_ws_y);
// Return position for one-past-last workspace too.
(0..=self.workspaces.len()).map(move |idx| {
let y = first_ws_y + idx as f64 * ws_height_with_gap;
- let loc = Point::from((0., y));
+ let loc = Point::from((0., y)) + static_offset;
Rectangle::new(loc, ws_size)
})
}
@@ -1026,20 +1176,42 @@ impl<W: LayoutElement> Monitor<W> {
Some((ws, geo))
}
+ pub fn workspace_under_narrow(
+ &self,
+ pos_within_output: Point<f64, Logical>,
+ ) -> Option<&Workspace<W>> {
+ self.workspaces_with_render_geo()
+ .find_map(|(ws, geo)| geo.contains(pos_within_output).then_some(ws))
+ }
+
pub fn window_under(&self, pos_within_output: Point<f64, Logical>) -> Option<(&W, HitType)> {
let (ws, geo) = self.workspace_under(pos_within_output)?;
- let (win, hit) = ws.window_under(pos_within_output - geo.loc)?;
- Some((win, hit.offset_win_pos(geo.loc)))
+
+ if self.overview_progress.is_some() {
+ let zoom = self.overview_zoom();
+ let pos_within_workspace = (pos_within_output - geo.loc).downscale(zoom);
+ let (win, hit) = ws.window_under(pos_within_workspace)?;
+ // During the overview animation, we cannot do input hits because we cannot really
+ // represent scaled windows properly.
+ Some((win, hit.to_activate()))
+ } else {
+ let (win, hit) = ws.window_under(pos_within_output - geo.loc)?;
+ Some((win, hit.offset_win_pos(geo.loc)))
+ }
}
pub fn resize_edges_under(&self, pos_within_output: Point<f64, Logical>) -> Option<ResizeEdge> {
+ if self.overview_progress.is_some() {
+ return None;
+ }
+
let (ws, geo) = self.workspace_under(pos_within_output)?;
ws.resize_edges_under(pos_within_output - geo.loc)
}
pub fn render_above_top_layer(&self) -> bool {
// Render above the top layer only if the view is stationary.
- if self.workspace_switch.is_some() {
+ if self.workspace_switch.is_some() || self.overview_progress.is_some() {
return false;
}
@@ -1074,7 +1246,7 @@ impl<W: LayoutElement> Monitor<W> {
// rendering for maximized GTK windows.
//
// FIXME: use proper bounds after fixing the Crop element.
- let crop_bounds = if self.workspace_switch.is_some() {
+ let crop_bounds = if self.workspace_switch.is_some() || self.overview_progress.is_some() {
Rectangle::new(
Point::from((-i32::MAX / 2, 0)),
Size::from((i32::MAX, height)),
@@ -1086,6 +1258,9 @@ impl<W: LayoutElement> Monitor<W> {
)
};
+ let zoom = self.overview_zoom();
+ let overview_clamped_progress = self.overview_progress.as_ref().map(|p| p.clamped_value());
+
// Draw the insert hint.
let mut insert_hint = None;
if !self.options.insert_hint.off {
@@ -1109,6 +1284,13 @@ impl<W: LayoutElement> Monitor<W> {
let floating = floating.filter_map(map_ws_contents);
let scrolling = scrolling.filter_map(map_ws_contents);
+ let shadow = overview_clamped_progress.map(|value| {
+ ws.render_shadow(renderer)
+ .map(move |elem| elem.with_alpha(value.clamp(0., 1.) as f32))
+ .map(MonitorInnerRenderElement::Shadow)
+ });
+ let shadow = shadow.into_iter().flatten();
+
let hint = if matches!(insert_hint, Some((hint_ws_id, _)) if hint_ws_id == ws.id()) {
let iter = insert_hint.take().unwrap().1;
let iter = iter.filter_map(move |elem| {
@@ -1122,9 +1304,10 @@ impl<W: LayoutElement> Monitor<W> {
};
let hint = hint.into_iter().flatten();
- let iter = floating.chain(hint).chain(scrolling);
+ let iter = floating.chain(hint).chain(scrolling).chain(shadow);
let iter = iter.map(move |elem| {
+ let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), zoom);
RelocateRenderElement::from_element(
elem,
// The offset we get from workspaces_with_render_positions() is already
@@ -1149,6 +1332,7 @@ impl<W: LayoutElement> Monitor<W> {
current_idx,
tracker: SwipeTracker::new(),
is_touchpad,
+ is_clamped: !self.overview_open,
};
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
}
@@ -1167,6 +1351,7 @@ impl<W: LayoutElement> Monitor<W> {
return None;
}
+ let zoom = self.overview_zoom();
let total_height = if gesture.is_touchpad {
WORKSPACE_GESTURE_MOVEMENT
} else {
@@ -1177,13 +1362,24 @@ impl<W: LayoutElement> Monitor<W> {
return None;
};
+ // Reduce the effect of zoom on the touchpad somewhat.
+ let delta_scale = if gesture.is_touchpad {
+ (zoom - 1.) / 2.5 + 1.
+ } else {
+ zoom
+ };
+
+ let delta_y = delta_y / delta_scale;
+ let mut rubber_band = WORKSPACE_GESTURE_RUBBER_BAND;
+ rubber_band.limit /= zoom;
+
gesture.tracker.push(delta_y, timestamp);
let pos = gesture.tracker.pos() / total_height;
let (min, max) = gesture.min_max(self.workspaces.len());
let new_idx = gesture.start_idx + pos;
- let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
+ let new_idx = rubber_band.clamp(min, max, new_idx);
if gesture.current_idx == new_idx {
return Some(false);
@@ -1202,6 +1398,7 @@ impl<W: LayoutElement> Monitor<W> {
return false;
}
+ let zoom = self.overview_zoom();
let total_height = if gesture.is_touchpad {
WORKSPACE_GESTURE_MOVEMENT
} else {
@@ -1216,6 +1413,9 @@ impl<W: LayoutElement> Monitor<W> {
let now = self.clock.now_unadjusted();
gesture.tracker.push(0., now);
+ let mut rubber_band = WORKSPACE_GESTURE_RUBBER_BAND;
+ rubber_band.limit /= zoom;
+
let mut velocity = gesture.tracker.velocity() / total_height;
let current_pos = gesture.tracker.pos() / total_height;
let pos = gesture.tracker.projected_end_pos() / total_height;
@@ -1223,14 +1423,10 @@ impl<W: LayoutElement> Monitor<W> {
let (min, max) = gesture.min_max(self.workspaces.len());
let new_idx = gesture.start_idx + pos;
- let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
+ let new_idx = new_idx.clamp(min, max);
let new_idx = new_idx.round() as usize;
- velocity *= WORKSPACE_GESTURE_RUBBER_BAND.clamp_derivative(
- min,
- max,
- gesture.start_idx + current_pos,
- );
+ velocity *= rubber_band.clamp_derivative(min, max, gesture.start_idx + current_pos);
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());