use std::cmp::min; use std::iter::zip; use std::rc::Rc; use std::time::Duration; use niri_config::CornerRadius; use smithay::backend::renderer::element::utils::{ CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement, }; use smithay::output::Output; use smithay::utils::{Logical, Point, Rectangle, Size}; use super::insert_hint_element::{InsertHintElement, InsertHintRenderElement}; use super::scrolling::{Column, ColumnWidth}; use super::tile::Tile; use super::workspace::{ compute_working_area, OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId, WorkspaceRenderElement, }; 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; use crate::utils::{ output_size, round_logical_in_physical, round_logical_in_physical_max1, ResizeEdge, }; /// Amount of touchpad movement to scroll the height of one workspace. const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.; const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand { stiffness: 0.5, limit: 0.05, }; #[derive(Debug)] pub struct Monitor { /// Output for this monitor. pub(super) output: Output, /// Cached name of the output. output_name: String, /// Latest known scale for this output. scale: smithay::output::Scale, /// Latest known size for this output. view_size: Size, /// Latest known working area for this output. /// /// Not rounded to physical pixels. // FIXME: since this is used for things like DnD scrolling edges in the overview, ideally this // should only consider overlay and top layer-shell surfaces. However, Smithay doesn't easily // let you do this at the moment. working_area: Rectangle, // Must always contain at least one. pub(super) workspaces: Vec>, /// Index of the currently active workspace. pub(super) active_workspace_idx: usize, /// ID of the previously active workspace. pub(super) previous_workspace_id: Option, /// In-progress switch between workspaces. pub(super) workspace_switch: Option, /// Indication where an interactively-moved window is about to be placed. pub(super) insert_hint: Option, /// Insert hint element for rendering. insert_hint_element: InsertHintElement, /// Location to render the insert hint element. insert_hint_render_loc: Option, /// Whether the overview is open. pub(super) overview_open: bool, /// Progress of the overview zoom animation, 1 is fully in overview. overview_progress: Option, /// Clock for driving animations. pub(super) clock: Clock, /// Configurable properties of the layout. pub(super) options: Rc, } #[derive(Debug)] pub enum WorkspaceSwitch { Animation(Animation), Gesture(WorkspaceSwitchGesture), } #[derive(Debug)] pub struct WorkspaceSwitchGesture { /// Index of the workspace where the gesture was started. center_idx: usize, /// Fractional workspace index where the gesture was started. /// /// Can differ from center_idx when starting a gesture in the middle between workspaces, for /// example by "catching" an animation. start_idx: f64, /// Current, fractional workspace index. pub(super) current_idx: f64, 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)] pub(super) enum InsertPosition { NewColumn(usize), InColumn(usize, usize), Floating, } #[derive(Debug)] pub(super) struct InsertHint { pub workspace: WorkspaceId, pub position: InsertPosition, pub corner_radius: CornerRadius, } #[derive(Debug, Clone, Copy)] struct InsertHintRenderLoc { workspace: WorkspaceId, location: Point, } #[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> { /// No particular preference. #[default] Auto, /// On this workspace. Workspace { /// Id of the target workspace. id: WorkspaceId, /// Override where the window will open as a new column. column_idx: Option, }, /// Next to this existing window. NextTo(&'a W::Id), } niri_render_elements! { MonitorInnerRenderElement => { Workspace = CropRenderElement>, InsertHint = CropRenderElement, Shadow = ShadowRenderElement, } } pub type MonitorRenderElement = RelocateRenderElement>>; impl WorkspaceSwitch { pub fn current_idx(&self) -> f64 { match self { WorkspaceSwitch::Animation(anim) => anim.value(), WorkspaceSwitch::Gesture(gesture) => gesture.current_idx, } } pub fn target_idx(&self) -> f64 { match self { WorkspaceSwitch::Animation(anim) => anim.to(), WorkspaceSwitch::Gesture(gesture) => gesture.current_idx, } } pub fn offset(&mut self, delta: isize) { match self { WorkspaceSwitch::Animation(anim) => anim.offset(delta as f64), WorkspaceSwitch::Gesture(gesture) => { if delta >= 0 { gesture.center_idx += delta as usize; } else { gesture.center_idx -= (-delta) as usize; } gesture.start_idx += delta as f64; gesture.current_idx += delta as f64; } } } /// Returns `true` if the workspace switch is [`Animation`]. /// /// [`Animation`]: WorkspaceSwitch::Animation #[must_use] fn is_animation(&self) -> bool { matches!(self, Self::Animation(..)) } } impl WorkspaceSwitchGesture { fn min_max(&self, workspace_count: usize) -> (f64, f64) { 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), } } } impl Monitor { pub fn new( output: Output, workspaces: Vec>, clock: Clock, options: Rc, ) -> Self { let scale = output.current_scale(); let view_size = output_size(&output); let working_area = compute_working_area(&output); Self { output_name: output.name(), output, scale, view_size, working_area, workspaces, active_workspace_idx: 0, previous_workspace_id: None, 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, } } pub fn output(&self) -> &Output { &self.output } pub fn output_name(&self) -> &String { &self.output_name } pub fn active_workspace_idx(&self) -> usize { self.active_workspace_idx } pub fn active_workspace_ref(&self) -> &Workspace { &self.workspaces[self.active_workspace_idx] } pub fn find_named_workspace(&self, workspace_name: &str) -> Option<&Workspace> { self.workspaces.iter().find(|ws| { ws.name .as_ref() .is_some_and(|name| name.eq_ignore_ascii_case(workspace_name)) }) } pub fn find_named_workspace_index(&self, workspace_name: &str) -> Option { self.workspaces.iter().position(|ws| { ws.name .as_ref() .is_some_and(|name| name.eq_ignore_ascii_case(workspace_name)) }) } pub fn active_workspace(&mut self) -> &mut Workspace { &mut self.workspaces[self.active_workspace_idx] } pub fn windows(&self) -> impl Iterator { self.workspaces.iter().flat_map(|ws| ws.windows()) } pub fn has_window(&self, window: &W::Id) -> bool { self.windows().any(|win| win.id() == window) } pub fn add_workspace_at(&mut self, idx: usize) { let ws = Workspace::new( self.output.clone(), self.clock.clone(), self.options.clone(), ); self.workspaces.insert(idx, ws); if idx <= self.active_workspace_idx { self.active_workspace_idx += 1; } if let Some(switch) = &mut self.workspace_switch { if idx as f64 <= switch.target_idx() { switch.offset(1); } } } pub fn add_workspace_top(&mut self) { self.add_workspace_at(0); } pub fn add_workspace_bottom(&mut self) { self.add_workspace_at(self.workspaces.len()); } 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, ) { if self.active_workspace_idx == idx { return; } // FIXME: also compute and use current velocity. let current_idx = self.workspace_render_idx(); self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id()); self.active_workspace_idx = idx; let config = config.unwrap_or(self.options.animations.workspace_switch.0); self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( self.clock.clone(), current_idx, idx as f64, 0., config, ))); } pub fn add_window( &mut self, window: W, target: MonitorAddWindowTarget, activate: ActivateWindow, width: ColumnWidth, is_full_width: bool, is_floating: bool, ) { // Currently, everything a workspace sets on a Tile is the same across all workspaces of a // monitor. So we can use any workspace, not necessarily the exact target workspace. let tile = self.workspaces[0].make_tile(window); self.add_tile(tile, target, activate, width, is_full_width, is_floating); } pub fn add_column(&mut self, mut workspace_idx: usize, column: Column, activate: bool) { let workspace = &mut self.workspaces[workspace_idx]; workspace.add_column(column, activate); // After adding a new window, workspace becomes this output's own. if workspace.name().is_none() { workspace.original_output = OutputId::new(&self.output); } if workspace_idx == self.workspaces.len() - 1 { self.add_workspace_bottom(); } if self.options.empty_workspace_above_first && workspace_idx == 0 { self.add_workspace_top(); workspace_idx += 1; } if activate { self.activate_workspace(workspace_idx); } } pub fn add_tile( &mut self, tile: Tile, target: MonitorAddWindowTarget, activate: ActivateWindow, width: ColumnWidth, is_full_width: bool, is_floating: bool, ) { let (mut workspace_idx, target) = match target { MonitorAddWindowTarget::Auto => { (self.active_workspace_idx, WorkspaceAddWindowTarget::Auto) } MonitorAddWindowTarget::Workspace { id, column_idx } => { let idx = self.workspaces.iter().position(|ws| ws.id() == id).unwrap(); let target = if let Some(column_idx) = column_idx { WorkspaceAddWindowTarget::NewColumnAt(column_idx) } else { WorkspaceAddWindowTarget::Auto }; (idx, target) } MonitorAddWindowTarget::NextTo(win_id) => { let idx = self .workspaces .iter_mut() .position(|ws| ws.has_window(win_id)) .unwrap(); (idx, WorkspaceAddWindowTarget::NextTo(win_id)) } }; let workspace = &mut self.workspaces[workspace_idx]; workspace.add_tile(tile, target, activate, width, is_full_width, is_floating); // After adding a new window, workspace becomes this output's own. if workspace.name().is_none() { workspace.original_output = OutputId::new(&self.output); } if workspace_idx == self.workspaces.len() - 1 { // Insert a new empty workspace. self.add_workspace_bottom(); } if self.options.empty_workspace_above_first && workspace_idx == 0 { self.add_workspace_top(); workspace_idx += 1; } if activate.map_smart(|| false) { self.activate_workspace(workspace_idx); } } pub fn add_tile_to_column( &mut self, workspace_idx: usize, column_idx: usize, tile_idx: Option, tile: Tile, activate: bool, ) { let workspace = &mut self.workspaces[workspace_idx]; workspace.add_tile_to_column(column_idx, tile_idx, tile, activate); // After adding a new window, workspace becomes this output's own. if workspace.name().is_none() { workspace.original_output = OutputId::new(&self.output); } // Since we're adding window to an existing column, the workspace isn't empty, and // therefore cannot be the last one, so we never need to insert a new empty workspace. if activate { self.activate_workspace(workspace_idx); } } pub fn clean_up_workspaces(&mut self) { assert!(self.workspace_switch.is_none()); let range_start = if self.options.empty_workspace_above_first { 1 } else { 0 }; for idx in (range_start..self.workspaces.len() - 1).rev() { if self.active_workspace_idx == idx { continue; } if !self.workspaces[idx].has_windows_or_name() { self.workspaces.remove(idx); if self.active_workspace_idx > idx { self.active_workspace_idx -= 1; } } } // Special case handling when empty_workspace_above_first is set and all workspaces // are empty. if self.options.empty_workspace_above_first && self.workspaces.len() == 2 { assert!(!self.workspaces[0].has_windows_or_name()); assert!(!self.workspaces[1].has_windows_or_name()); self.workspaces.remove(1); self.active_workspace_idx = 0; } } pub fn unname_workspace(&mut self, id: WorkspaceId) -> bool { let Some(ws) = self.workspaces.iter_mut().find(|ws| ws.id() == id) else { return false; }; ws.unname(); true } pub fn move_down_or_to_workspace_down(&mut self) { if !self.active_workspace().move_down() { self.move_to_workspace_down(); } } pub fn move_up_or_to_workspace_up(&mut self) { if !self.active_workspace().move_up() { self.move_to_workspace_up(); } } pub fn focus_window_or_workspace_down(&mut self) { if !self.active_workspace().focus_down() { self.switch_workspace_down(); } } pub fn focus_window_or_workspace_up(&mut self) { if !self.active_workspace().focus_up() { self.switch_workspace_up(); } } pub fn move_to_workspace_up(&mut self) { let source_workspace_idx = self.active_workspace_idx; let new_idx = source_workspace_idx.saturating_sub(1); if new_idx == source_workspace_idx { return; } let new_id = self.workspaces[new_idx].id(); let workspace = &mut self.workspaces[source_workspace_idx]; let Some(removed) = workspace.remove_active_tile(Transaction::new()) else { return; }; self.add_tile( removed.tile, MonitorAddWindowTarget::Workspace { id: new_id, column_idx: None, }, ActivateWindow::Yes, removed.width, removed.is_full_width, removed.is_floating, ); } pub fn move_to_workspace_down(&mut self) { let source_workspace_idx = self.active_workspace_idx; let new_idx = min(source_workspace_idx + 1, self.workspaces.len() - 1); if new_idx == source_workspace_idx { return; } let new_id = self.workspaces[new_idx].id(); let workspace = &mut self.workspaces[source_workspace_idx]; let Some(removed) = workspace.remove_active_tile(Transaction::new()) else { return; }; self.add_tile( removed.tile, MonitorAddWindowTarget::Workspace { id: new_id, column_idx: None, }, ActivateWindow::Yes, removed.width, removed.is_full_width, removed.is_floating, ); } pub fn move_to_workspace( &mut self, window: Option<&W::Id>, idx: usize, activate: ActivateWindow, ) { let source_workspace_idx = if let Some(window) = window { self.workspaces .iter() .position(|ws| ws.has_window(window)) .unwrap() } else { self.active_workspace_idx }; let new_idx = min(idx, self.workspaces.len() - 1); if new_idx == source_workspace_idx { return; } let new_id = self.workspaces[new_idx].id(); let activate = activate.map_smart(|| { window.map_or(true, |win| { self.active_window().map(|win| win.id()) == Some(win) }) }); let workspace = &mut self.workspaces[source_workspace_idx]; let transaction = Transaction::new(); let removed = if let Some(window) = window { workspace.remove_tile(window, transaction) } else if let Some(removed) = workspace.remove_active_tile(transaction) { removed } else { return; }; self.add_tile( removed.tile, MonitorAddWindowTarget::Workspace { id: new_id, column_idx: None, }, if activate { ActivateWindow::Yes } else { ActivateWindow::No }, removed.width, removed.is_full_width, removed.is_floating, ); if self.workspace_switch.is_none() { self.clean_up_workspaces(); } } pub fn move_column_to_workspace_up(&mut self) { let source_workspace_idx = self.active_workspace_idx; let new_idx = source_workspace_idx.saturating_sub(1); if new_idx == source_workspace_idx { return; } let workspace = &mut self.workspaces[source_workspace_idx]; if workspace.floating_is_active() { self.move_to_workspace_up(); return; } let Some(column) = workspace.remove_active_column() else { return; }; self.add_column(new_idx, column, true); } pub fn move_column_to_workspace_down(&mut self) { let source_workspace_idx = self.active_workspace_idx; let new_idx = min(source_workspace_idx + 1, self.workspaces.len() - 1); if new_idx == source_workspace_idx { return; } let workspace = &mut self.workspaces[source_workspace_idx]; if workspace.floating_is_active() { self.move_to_workspace_down(); return; } let Some(column) = workspace.remove_active_column() else { return; }; self.add_column(new_idx, column, true); } pub fn move_column_to_workspace(&mut self, idx: usize) { let source_workspace_idx = self.active_workspace_idx; let new_idx = min(idx, self.workspaces.len() - 1); if new_idx == source_workspace_idx { return; } let workspace = &mut self.workspaces[source_workspace_idx]; if workspace.floating_is_active() { self.move_to_workspace(None, idx, ActivateWindow::Smart); return; } let Some(column) = workspace.remove_active_column() else { return; }; self.add_column(new_idx, column, true); } pub fn switch_workspace_up(&mut self) { self.activate_workspace(self.active_workspace_idx.saturating_sub(1)); } pub fn switch_workspace_down(&mut self) { self.activate_workspace(min( self.active_workspace_idx + 1, self.workspaces.len() - 1, )); } fn previous_workspace_idx(&self) -> Option { let id = self.previous_workspace_id?; self.workspaces.iter().position(|w| w.id() == id) } pub fn switch_workspace(&mut self, idx: usize) { self.activate_workspace(min(idx, self.workspaces.len() - 1)); } pub fn switch_workspace_auto_back_and_forth(&mut self, idx: usize) { let idx = min(idx, self.workspaces.len() - 1); if idx == self.active_workspace_idx { if let Some(prev_idx) = self.previous_workspace_idx() { self.switch_workspace(prev_idx); } } else { self.switch_workspace(idx); } } pub fn switch_workspace_previous(&mut self) { if let Some(idx) = self.previous_workspace_idx() { self.switch_workspace(idx); } } pub fn active_window(&self) -> Option<&W> { self.active_workspace_ref().active_window() } 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(); } } for ws in &mut self.workspaces { ws.advance_animations(); } } pub(super) fn are_animations_ongoing(&self) -> bool { self.workspace_switch .as_ref() .is_some_and(|s| s.is_animation()) || self.workspaces.iter().any(|ws| ws.are_animations_ongoing()) } pub fn are_transitions_ongoing(&self) -> bool { self.workspace_switch.is_some() || self .workspaces .iter() .any(|ws| ws.are_transitions_ongoing()) } pub fn update_render_elements(&mut self, is_active: bool) { let mut insert_hint_ws_geo = None; let insert_hint_ws_id = self.insert_hint.as_ref().map(|hint| hint.workspace); for (ws, geo) in self.workspaces_with_render_geo_mut() { ws.update_render_elements(is_active); if Some(ws.id()) == insert_hint_ws_id { insert_hint_ws_geo = Some(geo); } } self.insert_hint_render_loc = None; if let Some(hint) = &self.insert_hint { if let Some(ws) = self.workspaces.iter().find(|ws| ws.id() == hint.workspace) { if let Some(mut area) = ws.insert_hint_area(hint.position) { let scale = ws.scale().fractional_scale(); let view_size = ws.view_size(); // 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.); } // Round to physical pixels. area = area.to_physical_precise_round(scale).to_logical(scale); let view_rect = Rectangle::new(area.loc.upscale(-1.), view_size); self.insert_hint_element.update_render_elements( area.size, view_rect, hint.corner_radius, scale, ); self.insert_hint_render_loc = Some(InsertHintRenderLoc { workspace: hint.workspace, location: area.loc, }); } } else { error!("insert hint workspace missing from monitor"); } } } pub fn update_config(&mut self, options: Rc) { if self.options.empty_workspace_above_first != options.empty_workspace_above_first && self.workspaces.len() > 1 { if options.empty_workspace_above_first { self.add_workspace_top(); } else if self.workspace_switch.is_none() && self.active_workspace_idx != 0 { self.workspaces.remove(0); self.active_workspace_idx = self.active_workspace_idx.saturating_sub(1); } } for ws in &mut self.workspaces { ws.update_config(options.clone()); } self.insert_hint_element.update_config(options.insert_hint); self.options = options; } pub fn update_shaders(&mut self) { for ws in &mut self.workspaces { ws.update_shaders(); } self.insert_hint_element.update_shaders(); } pub fn update_output_size(&mut self) { self.scale = self.output.current_scale(); self.view_size = output_size(&self.output); self.working_area = compute_working_area(&self.output); for ws in &mut self.workspaces { ws.update_output_size(); } } pub fn move_workspace_down(&mut self) { let mut new_idx = min(self.active_workspace_idx + 1, self.workspaces.len() - 1); if new_idx == self.active_workspace_idx { return; } self.workspaces.swap(self.active_workspace_idx, new_idx); if new_idx == self.workspaces.len() - 1 { // Insert a new empty workspace. self.add_workspace_bottom(); } if self.options.empty_workspace_above_first && self.active_workspace_idx == 0 { self.add_workspace_top(); new_idx += 1; } let previous_workspace_id = self.previous_workspace_id; self.activate_workspace(new_idx); self.workspace_switch = None; self.previous_workspace_id = previous_workspace_id; self.clean_up_workspaces(); } pub fn move_workspace_up(&mut self) { let mut new_idx = self.active_workspace_idx.saturating_sub(1); if new_idx == self.active_workspace_idx { return; } self.workspaces.swap(self.active_workspace_idx, new_idx); if self.active_workspace_idx == self.workspaces.len() - 1 { // Insert a new empty workspace. self.add_workspace_bottom(); } if self.options.empty_workspace_above_first && new_idx == 0 { self.add_workspace_top(); new_idx += 1; } let previous_workspace_id = self.previous_workspace_id; self.activate_workspace(new_idx); self.workspace_switch = None; self.previous_workspace_id = previous_workspace_id; self.clean_up_workspaces(); } pub fn move_workspace_to_idx(&mut self, old_idx: usize, new_idx: usize) { if self.workspaces.len() <= old_idx { return; } let mut new_idx = new_idx.clamp(0, self.workspaces.len() - 1); if old_idx == new_idx { return; } let ws = self.workspaces.remove(old_idx); self.workspaces.insert(new_idx, ws); if new_idx > old_idx { if new_idx == self.workspaces.len() - 1 { // Insert a new empty workspace. self.add_workspace_bottom(); } if self.options.empty_workspace_above_first && old_idx == 0 { self.add_workspace_top(); new_idx += 1; } } else { if old_idx == self.workspaces.len() - 1 { // Insert a new empty workspace. self.add_workspace_bottom(); } if self.options.empty_workspace_above_first && new_idx == 0 { self.add_workspace_top(); new_idx += 1; } } // Only refocus the workspace if it was already focused if self.active_workspace_idx == old_idx { self.active_workspace_idx = new_idx; // If the workspace order was switched so that the current workspace moved down the // workspace stack, focus correctly } else if new_idx <= self.active_workspace_idx && old_idx > self.active_workspace_idx { self.active_workspace_idx += 1; } else if new_idx >= self.active_workspace_idx && old_idx < self.active_workspace_idx { self.active_workspace_idx = self.active_workspace_idx.saturating_sub(1); } self.workspace_switch = None; self.clean_up_workspaces(); } /// Returns the geometry of the active tile relative to and clamped to the output. /// /// During animations, assumes the final view position. pub fn active_tile_visual_rectangle(&self) -> Option> { if self.overview_open { return None; } self.active_workspace_ref().active_tile_visual_rectangle() } fn workspace_size(&self, zoom: f64) -> Size { let ws_size = self.view_size.upscale(zoom); let scale = self.scale.fractional_scale(); ws_size.to_physical_precise_ceil(scale).to_logical(scale) } fn workspace_gap(&self, zoom: f64) -> f64 { let scale = self.scale.fractional_scale(); let gap = self.view_size.h * 0.1 * zoom; round_logical_in_physical_max1(scale, gap) } fn workspace_size_with_gap(&self, zoom: f64) -> Size { let gap = self.workspace_gap(zoom); 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 { 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 { self.active_workspace_idx as f64 } } pub fn workspaces_render_geo(&self) -> impl Iterator> { let scale = self.scale.fractional_scale(); 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)) + static_offset; Rectangle::new(loc, ws_size) }) } pub fn workspaces_with_render_geo( &self, ) -> impl Iterator, Rectangle)> { let output_geo = Rectangle::from_size(self.view_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, Rectangle)> { let output_geo = Rectangle::from_size(self.view_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, ) -> Option<(&Workspace, Rectangle)> { 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((self.view_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, ) -> Option<&Workspace> { 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) -> Option<(&W, HitType)> { let (ws, geo) = self.workspace_under(pos_within_output)?; 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) -> Option { 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() || self.overview_progress.is_some() { return false; } let ws = &self.workspaces[self.active_workspace_idx]; ws.render_above_top_layer() } pub fn render_elements<'a, R: NiriRenderer>( &'a self, renderer: &'a mut R, target: RenderTarget, focus_ring: bool, ) -> impl Iterator< Item = ( Rectangle, impl Iterator> + 'a, ), > { let _span = tracy_client::span!("Monitor::render_elements"); let scale = self.scale.fractional_scale(); // Ceil the height in physical pixels. let height = (self.view_size.h * scale).ceil() as i32; // Crop the elements to prevent them overflowing, currently visible during a workspace // switch. // // HACK: crop to infinite bounds at least horizontally where we // know there's no workspace joining or monitor bounds, otherwise // it will cut pixel shaders and mess up the coordinate space. // There's also a damage tracking bug which causes glitched // rendering for maximized GTK windows. // // FIXME: use proper bounds after fixing the Crop element. 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)), ) } else { Rectangle::new( Point::from((-i32::MAX / 2, -i32::MAX / 2)), Size::from((i32::MAX, i32::MAX)), ) }; 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 { if let Some(render_loc) = self.insert_hint_render_loc { insert_hint = Some(( render_loc.workspace, self.insert_hint_element .render(renderer, render_loc.location), )); } } self.workspaces_with_render_geo().map(move |(ws, geo)| { let map_ws_contents = move |elem: WorkspaceRenderElement| { let elem = CropRenderElement::from_element(elem, scale, crop_bounds)?; let elem = MonitorInnerRenderElement::Workspace(elem); Some(elem) }; let (floating, scrolling) = ws.render_elements(renderer, target, focus_ring); 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| { let elem = CropRenderElement::from_element(elem, scale, crop_bounds)?; let elem = MonitorInnerRenderElement::InsertHint(elem); Some(elem) }); Some(iter) } else { None }; let hint = hint.into_iter().flatten(); 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 // 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, ) }); (geo, iter) }) } pub fn workspace_switch_gesture_begin(&mut self, is_touchpad: bool) { let center_idx = self.active_workspace_idx; let current_idx = self.workspace_render_idx(); let gesture = WorkspaceSwitchGesture { center_idx, start_idx: current_idx, current_idx, tracker: SwipeTracker::new(), is_touchpad, is_clamped: !self.overview_open, }; self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture)); } pub fn workspace_switch_gesture_update( &mut self, delta_y: f64, timestamp: Duration, is_touchpad: bool, ) -> Option { let Some(WorkspaceSwitch::Gesture(gesture)) = &self.workspace_switch else { return None; }; if gesture.is_touchpad != is_touchpad { return None; } let zoom = self.overview_zoom(); let total_height = if gesture.is_touchpad { WORKSPACE_GESTURE_MOVEMENT } else { self.workspace_size_with_gap(1.).h }; let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else { 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 = rubber_band.clamp(min, max, new_idx); if gesture.current_idx == new_idx { return Some(false); } gesture.current_idx = new_idx; Some(true) } pub fn workspace_switch_gesture_end(&mut self, is_touchpad: Option) -> bool { let Some(WorkspaceSwitch::Gesture(gesture)) = &self.workspace_switch else { return false; }; if is_touchpad.is_some_and(|x| gesture.is_touchpad != x) { return false; } let zoom = self.overview_zoom(); let total_height = if gesture.is_touchpad { WORKSPACE_GESTURE_MOVEMENT } else { self.workspace_size_with_gap(1.).h }; let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else { return false; }; // Take into account any idle time between the last event and now. let now = self.clock.now_unadjusted(); gesture.tracker.push(0., now); let 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; let (min, max) = gesture.min_max(self.workspaces.len()); let new_idx = gesture.start_idx + pos; let new_idx = new_idx.clamp(min, max); let new_idx = new_idx.round() as usize; velocity *= rubber_band.clamp_derivative(min, max, gesture.start_idx + current_pos); self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id()); self.active_workspace_idx = new_idx; self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( self.clock.clone(), gesture.current_idx, new_idx as f64, velocity, self.options.animations.workspace_switch.0, ))); true } pub fn scale(&self) -> smithay::output::Scale { self.scale } pub fn view_size(&self) -> Size { self.view_size } pub fn working_area(&self) -> Rectangle { self.working_area } }