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, }; 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::{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::RenderTarget; use crate::rubber_band::RubberBand; use crate::utils::transaction::Transaction; use crate::utils::{output_size, 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, /// 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, } #[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, } /// 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, } } 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) { let min = self.center_idx.saturating_sub(1) as f64; let max = (self.center_idx + 1).min(workspace_count - 1) as f64; (min, max) } } 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, 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()); } fn activate_workspace(&mut self, idx: usize) { 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; self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( self.clock.clone(), current_idx, idx as f64, 0., self.options.animations.workspace_switch.0, ))); } 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 geo = insert_hint_ws_geo.unwrap(); 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> { self.active_workspace_ref().active_tile_visual_rectangle() } pub fn workspace_render_idx(&self) -> f64 { 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 size = self.view_size; // Ceil the workspace size in physical pixels. let ws_size = size.to_physical_precise_ceil(scale).to_logical(scale); let first_ws_y = -self.workspace_render_idx() * ws_size.h; (0..self.workspaces.len()).map(move |idx| { let y = first_ws_y + idx as f64 * ws_size.h; let loc = Point::from((0., y)); let loc = loc.to_physical_precise_round(scale).to_logical(scale); 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 window_under(&self, pos_within_output: Point) -> 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))) } pub fn resize_edges_under(&self, pos_within_output: Point) -> Option { 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() { 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> + '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() { 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)), ) }; // 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() .flat_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 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); iter.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, ) }) }) } 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, }; 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)) = &mut self.workspace_switch else { return None; }; if gesture.is_touchpad != is_touchpad { return None; } gesture.tracker.push(delta_y, timestamp); let total_height = if gesture.is_touchpad { WORKSPACE_GESTURE_MOVEMENT } else { self.workspaces[0].view_size().h }; 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); 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)) = &mut self.workspace_switch else { return false; }; if is_touchpad.is_some_and(|x| gesture.is_touchpad != x) { 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 total_height = if gesture.is_touchpad { WORKSPACE_GESTURE_MOVEMENT } else { self.workspaces[0].view_size().h }; 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 = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx); let new_idx = new_idx.round() as usize; velocity *= WORKSPACE_GESTURE_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 } }