use std::cmp::max; use std::rc::Rc; use std::time::Duration; use niri_config::{ CenterFocusedColumn, CornerRadius, 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}; use smithay::output::Output; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::{Logical, Point, Rectangle, Serial, Size, Transform}; use smithay::wayland::compositor::with_states; use smithay::wayland::shell::xdg::SurfaceCachedState; use super::floating::{FloatingSpace, FloatingSpaceRenderElement}; use super::scrolling::{ Column, ColumnWidth, ScrollDirection, ScrollingSpace, ScrollingSpaceRenderElement, }; use super::shadow::Shadow; use super::tile::{Tile, TileRenderSnapshot}; use super::{ ActivateWindow, HitType, InsertPosition, InteractiveResizeData, LayoutElement, Options, RemovedTile, SizeFrac, }; 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}; use crate::utils::{ ensure_min_max_size, ensure_min_max_size_maybe_zero, output_size, send_scale_transform, ResizeEdge, }; use crate::window::ResolvedWindowRules; #[derive(Debug)] pub struct Workspace { /// The scrollable-tiling layout. scrolling: ScrollingSpace, /// The floating layout. floating: FloatingSpace, /// Whether the floating layout is active instead of the scrolling layout. floating_is_active: FloatingActive, /// The original output of this workspace. /// /// Most of the time this will be the workspace's current output, however, after an output /// disconnection, it may remain pointing to the disconnected output. pub(super) original_output: OutputId, /// Current output of this workspace. output: Option, /// Latest known output scale for this workspace. /// /// This should be set from the current workspace output, or, if all outputs have been /// disconnected, preserved until a new output is connected. scale: smithay::output::Scale, /// Latest known output transform for this workspace. /// /// This should be set from the current workspace output, or, if all outputs have been /// disconnected, preserved until a new output is connected. transform: Transform, /// Latest known view size for this workspace. /// /// This should be computed from the current workspace output size, or, if all outputs have /// been disconnected, preserved until a new output is connected. view_size: Size, /// Latest known working area for this workspace. /// /// Not rounded to physical pixels. /// /// This is similar to view size, but takes into account things like layer shell exclusive /// zones. working_area: Rectangle, /// This workspace's shadow in the overview. shadow: Shadow, /// Clock for driving animations. pub(super) clock: Clock, /// Configurable properties of the layout as received from the parent monitor. pub(super) base_options: Rc, /// Configurable properties of the layout with logical sizes adjusted for the current `scale`. pub(super) options: Rc, /// Optional name of this workspace. pub(super) name: Option, /// Unique ID of this workspace. id: WorkspaceId, } #[derive(Debug, Clone)] pub struct OutputId(String); impl OutputId { pub fn matches(&self, output: &Output) -> bool { let output_name = output.user_data().get::().unwrap(); output_name.matches(&self.0) } } static WORKSPACE_ID_COUNTER: IdCounter = IdCounter::new(); #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct WorkspaceId(u64); impl WorkspaceId { fn next() -> WorkspaceId { WorkspaceId(WORKSPACE_ID_COUNTER.next()) } pub fn get(self) -> u64 { self.0 } pub fn specific(id: u64) -> Self { Self(id) } } niri_render_elements! { WorkspaceRenderElement => { Scrolling = ScrollingSpaceRenderElement, Floating = FloatingSpaceRenderElement, } } #[derive(Debug)] pub(super) struct InteractiveResize { pub window: W::Id, pub original_window_size: Size, pub data: InteractiveResizeData, } /// Resolved width or height in logical pixels. #[derive(Debug, Clone, Copy)] pub enum ResolvedSize { /// Size of the tile including borders. Tile(f64), /// Size of the window excluding borders. Window(f64), } /// Whether the floating space is active. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum FloatingActive { /// The scrolling space is active. No, /// The scrolling space is active, but the floating space should render on top, even if the /// active scrolling window is fullscreen. /// /// This is necessary for focus-follows-mouse that activates but doesn't raise the window to /// avoid being annoying. NoButRaised, /// The floating space is active. Yes, } /// Where to put a newly added window. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum WorkspaceAddWindowTarget<'a, W: LayoutElement> { /// No particular preference. #[default] Auto, /// As a new column at this index. NewColumnAt(usize), /// Next to this existing window. NextTo(&'a W::Id), } impl OutputId { pub fn new(output: &Output) -> Self { let output_name = output.user_data().get::().unwrap(); Self(output_name.format_make_model_serial_or_connector()) } } impl FloatingActive { fn get(self) -> bool { self == Self::Yes } } impl Workspace { pub fn new(output: Output, clock: Clock, options: Rc) -> Self { Self::new_with_config(output, None, clock, options) } pub fn new_with_config( output: Output, config: Option, clock: Clock, base_options: Rc, ) -> Self { let original_output = config .as_ref() .and_then(|c| c.open_on_output.clone()) .map(OutputId) .unwrap_or(OutputId::new(&output)); let scale = output.current_scale(); let options = Rc::new(Options::clone(&base_options).adjusted_for_scale(scale.fractional_scale())); let view_size = output_size(&output); let working_area = compute_working_area(&output); let scrolling = ScrollingSpace::new( view_size, working_area, scale.fractional_scale(), clock.clone(), options.clone(), ); let floating = FloatingSpace::new( view_size, working_area, scale.fractional_scale(), clock.clone(), options.clone(), ); let shadow_config = compute_workspace_shadow_config(options.overview.workspace_shadow, view_size); Self { scrolling, floating, floating_is_active: FloatingActive::No, original_output, scale, transform: output.current_transform(), view_size, working_area, shadow: Shadow::new(shadow_config), output: Some(output), clock, base_options, options, name: config.map(|c| c.name.0), id: WorkspaceId::next(), } } pub fn new_with_config_no_outputs( config: Option, clock: Clock, base_options: Rc, ) -> Self { let original_output = OutputId( config .as_ref() .and_then(|c| c.open_on_output.clone()) .unwrap_or_default(), ); let scale = smithay::output::Scale::Integer(1); let options = Rc::new(Options::clone(&base_options).adjusted_for_scale(scale.fractional_scale())); let view_size = Size::from((1280., 720.)); let working_area = Rectangle::from_size(Size::from((1280., 720.))); let scrolling = ScrollingSpace::new( view_size, working_area, scale.fractional_scale(), clock.clone(), options.clone(), ); let floating = FloatingSpace::new( view_size, working_area, scale.fractional_scale(), clock.clone(), options.clone(), ); let shadow_config = compute_workspace_shadow_config(options.overview.workspace_shadow, view_size); Self { scrolling, floating, floating_is_active: FloatingActive::No, output: None, scale, transform: Transform::Normal, original_output, view_size, working_area, shadow: Shadow::new(shadow_config), clock, base_options, options, name: config.map(|c| c.name.0), id: WorkspaceId::next(), } } pub fn new_no_outputs(clock: Clock, options: Rc) -> Self { Self::new_with_config_no_outputs(None, clock, options) } pub fn id(&self) -> WorkspaceId { self.id } pub fn name(&self) -> Option<&String> { self.name.as_ref() } pub fn unname(&mut self) { self.name = None; } pub fn has_windows_or_name(&self) -> bool { self.has_windows() || self.name.is_some() } pub fn scale(&self) -> smithay::output::Scale { self.scale } pub fn advance_animations(&mut self) { self.scrolling.advance_animations(); self.floating.advance_animations(); } pub fn are_animations_ongoing(&self) -> bool { self.scrolling.are_animations_ongoing() || self.floating.are_animations_ongoing() } pub fn are_transitions_ongoing(&self) -> bool { 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()); let view_rect = Rectangle::from_size(self.view_size); self.floating .update_render_elements(is_active && self.floating_is_active.get(), view_rect); self.shadow.update_render_elements( self.view_size, true, CornerRadius::default(), self.scale.fractional_scale(), 1., ); } pub fn update_config(&mut self, base_options: Rc) { let scale = self.scale.fractional_scale(); let options = Rc::new(Options::clone(&base_options).adjusted_for_scale(scale)); self.scrolling.update_config( self.view_size, self.working_area, self.scale.fractional_scale(), options.clone(), ); self.floating.update_config( self.view_size, self.working_area, self.scale.fractional_scale(), options.clone(), ); let shadow_config = compute_workspace_shadow_config(options.overview.workspace_shadow, self.view_size); self.shadow.update_config(shadow_config); self.base_options = base_options; self.options = options; } pub fn update_shaders(&mut self) { self.scrolling.update_shaders(); self.floating.update_shaders(); self.shadow.update_shaders(); } pub fn windows(&self) -> impl Iterator + '_ { self.tiles().map(Tile::window) } pub fn windows_mut(&mut self) -> impl Iterator + '_ { self.tiles_mut().map(Tile::window_mut) } pub fn tiles(&self) -> impl Iterator> + '_ { let scrolling = self.scrolling.tiles(); let floating = self.floating.tiles(); scrolling.chain(floating) } pub fn tiles_mut(&mut self) -> impl Iterator> + '_ { let scrolling = self.scrolling.tiles_mut(); let floating = self.floating.tiles_mut(); scrolling.chain(floating) } pub fn is_floating(&self, id: &W::Id) -> bool { self.floating.has_window(id) } pub fn current_output(&self) -> Option<&Output> { self.output.as_ref() } pub fn active_window(&self) -> Option<&W> { if self.floating_is_active.get() { self.floating.active_window() } else { self.scrolling.active_window() } } pub fn active_window_mut(&mut self) -> Option<&mut W> { if self.floating_is_active.get() { self.floating.active_window_mut() } else { self.scrolling.active_window_mut() } } pub fn is_active_fullscreen(&self) -> bool { self.scrolling.is_active_fullscreen() } pub fn set_output(&mut self, output: Option) { if self.output == output { return; } if let Some(output) = self.output.take() { for win in self.windows() { win.output_leave(&output); } } self.output = output; if let Some(output) = &self.output { // Normalize original output: possibly replace connector with make/model/serial. if self.original_output.matches(output) { self.original_output = OutputId::new(output); } self.update_output_size(); for win in self.windows() { self.enter_output_for_window(win); } } } fn enter_output_for_window(&self, window: &W) { if let Some(output) = &self.output { window.set_preferred_scale_transform(self.scale, self.transform); window.output_enter(output); } } pub fn update_output_size(&mut self) { let output = self.output.as_ref().unwrap(); let scale = output.current_scale(); let transform = output.current_transform(); let view_size = output_size(output); let working_area = compute_working_area(output); self.set_view_size(scale, transform, view_size, working_area); } fn set_view_size( &mut self, scale: smithay::output::Scale, transform: Transform, size: Size, working_area: Rectangle, ) { let scale_transform_changed = self.transform != transform || self.scale.integer_scale() != scale.integer_scale() || self.scale.fractional_scale() != scale.fractional_scale(); if !scale_transform_changed && self.view_size == size && self.working_area == working_area { return; } let fractional_scale_changed = self.scale.fractional_scale() != scale.fractional_scale(); self.scale = scale; self.transform = transform; self.view_size = size; self.working_area = working_area; if fractional_scale_changed { // Options need to be recomputed for the new scale. self.update_config(self.base_options.clone()); } else { // Pass our existing options as is. self.scrolling.update_config( size, working_area, scale.fractional_scale(), self.options.clone(), ); self.floating.update_config( size, working_area, scale.fractional_scale(), self.options.clone(), ); let shadow_config = compute_workspace_shadow_config(self.options.overview.workspace_shadow, size); self.shadow.update_config(shadow_config); } if scale_transform_changed { for window in self.windows() { window.set_preferred_scale_transform(self.scale, self.transform); } } } pub fn view_size(&self) -> Size { self.view_size } pub fn make_tile(&self, window: W) -> Tile { Tile::new( window, self.view_size, self.scale.fractional_scale(), self.clock.clone(), self.options.clone(), ) } pub fn add_tile( &mut self, mut tile: Tile, target: WorkspaceAddWindowTarget, activate: ActivateWindow, width: ColumnWidth, is_full_width: bool, is_floating: bool, ) { self.enter_output_for_window(tile.window()); tile.unfullscreen_to_floating = is_floating; match target { WorkspaceAddWindowTarget::Auto => { // Don't steal focus from an active fullscreen window. let activate = activate.map_smart(|| !self.is_active_fullscreen()); // If the tile is pending fullscreen, open it in the scrolling layout where it can // go fullscreen. if is_floating && !tile.window().is_pending_fullscreen() { self.floating.add_tile(tile, activate); if activate || self.scrolling.is_empty() { self.floating_is_active = FloatingActive::Yes; } } else { self.scrolling .add_tile(None, tile, activate, width, is_full_width, None); if activate { self.floating_is_active = FloatingActive::No; } } } WorkspaceAddWindowTarget::NewColumnAt(col_idx) => { let activate = activate.map_smart(|| false); self.scrolling .add_tile(Some(col_idx), tile, activate, width, is_full_width, None); if activate { self.floating_is_active = FloatingActive::No; } } WorkspaceAddWindowTarget::NextTo(next_to) => { let activate = activate.map_smart(|| self.active_window().unwrap().id() == next_to); let floating_has_window = self.floating.has_window(next_to); if is_floating && !tile.window().is_pending_fullscreen() { if floating_has_window { self.floating.add_tile_above(next_to, tile, activate); } else { // FIXME: use static pos let (next_to_tile, render_pos, _visible) = self .scrolling .tiles_with_render_positions() .find(|(tile, _, _)| tile.window().id() == next_to) .unwrap(); // Position the new tile in the center above the next_to tile. Think a // dialog opening on top of a window. let tile_size = tile.tile_size(); let pos = render_pos + (next_to_tile.tile_size().to_point() - tile_size.to_point()) .downscale(2.); let pos = self.floating.clamp_within_working_area(pos, tile_size); let pos = self.floating.logical_to_size_frac(pos); tile.floating_pos = Some(pos); self.floating.add_tile(tile, activate); } if activate || self.scrolling.is_empty() { self.floating_is_active = FloatingActive::Yes; } } else if floating_has_window { self.scrolling .add_tile(None, tile, activate, width, is_full_width, None); if activate { self.floating_is_active = FloatingActive::No; } } else { self.scrolling .add_tile_right_of(next_to, tile, activate, width, is_full_width); if activate { self.floating_is_active = FloatingActive::No; } } } } } pub fn add_tile_to_column( &mut self, col_idx: usize, tile_idx: Option, tile: Tile, activate: bool, ) { self.enter_output_for_window(tile.window()); self.scrolling .add_tile_to_column(col_idx, tile_idx, tile, activate); if activate { self.floating_is_active = FloatingActive::No; } } pub fn add_column(&mut self, column: Column, activate: bool) { for (tile, _) in column.tiles() { self.enter_output_for_window(tile.window()); } self.scrolling.add_column(None, column, activate, None); if activate { self.floating_is_active = FloatingActive::No; } } fn update_focus_floating_tiling_after_removing(&mut self, removed_from_floating: bool) { if removed_from_floating { if self.floating.is_empty() { self.floating_is_active = FloatingActive::No; } } else { // Scrolling should remain focused if both are empty. if self.scrolling.is_empty() && !self.floating.is_empty() { self.floating_is_active = FloatingActive::Yes; } } } pub fn remove_tile(&mut self, id: &W::Id, transaction: Transaction) -> RemovedTile { let mut from_floating = false; let removed = if self.floating.has_window(id) { from_floating = true; self.floating.remove_tile(id) } else { self.scrolling.remove_tile(id, transaction) }; if let Some(output) = &self.output { removed.tile.window().output_leave(output); } self.update_focus_floating_tiling_after_removing(from_floating); removed } pub fn remove_active_tile(&mut self, transaction: Transaction) -> Option> { let from_floating = self.floating_is_active.get(); let removed = if from_floating { self.floating.remove_active_tile()? } else { self.scrolling.remove_active_tile(transaction)? }; if let Some(output) = &self.output { removed.tile.window().output_leave(output); } self.update_focus_floating_tiling_after_removing(from_floating); Some(removed) } pub fn remove_active_column(&mut self) -> Option> { let from_floating = self.floating_is_active.get(); if from_floating { return None; } let column = self.scrolling.remove_active_column()?; if let Some(output) = &self.output { for (tile, _) in column.tiles() { tile.window().output_leave(output); } } self.update_focus_floating_tiling_after_removing(from_floating); Some(column) } pub fn resolve_default_width( &self, default_width: Option>, is_floating: bool, ) -> Option { match default_width { Some(Some(width)) => Some(width), Some(None) => None, None if is_floating => None, None => self.options.default_column_width, } } pub fn resolve_default_height( &self, default_height: Option>, is_floating: bool, ) -> Option { match default_height { Some(Some(height)) => Some(height), Some(None) => None, None if is_floating => None, // We don't have a global default at the moment. None => None, } } pub fn new_window_size( &self, width: Option, height: Option, is_floating: bool, rules: &ResolvedWindowRules, (min_size, max_size): (Size, Size), ) -> Size { let mut size = if is_floating { self.floating.new_window_size(width, height, rules) } else { self.scrolling.new_window_size(width, height, rules) }; // If the window has a fixed size, or we're picking some fixed size, apply min and max // size. This is to ensure that a fixed-size window rule works on open, while still // allowing the window freedom to pick its default size otherwise. let (min_size, max_size) = rules.apply_min_max_size(min_size, max_size); size.w = ensure_min_max_size_maybe_zero(size.w, min_size.w, max_size.w); // For scrolling (where height is > 0) only ensure fixed height, since at runtime scrolling // will only honor fixed height currently. if min_size.h == max_size.h { size.h = ensure_min_max_size(size.h, min_size.h, max_size.h); } else if size.h > 0 { // Also always honor min height, scrolling always does. size.h = max(size.h, min_size.h); } size } pub fn configure_new_window( &self, window: &Window, width: Option, height: Option, is_floating: bool, rules: &ResolvedWindowRules, ) { window.with_surfaces(|surface, data| { send_scale_transform(surface, data, self.scale, self.transform); }); let toplevel = window.toplevel().expect("no x11 support"); let (min_size, max_size) = with_states(toplevel.wl_surface(), |state| { let mut guard = state.cached_state.get::(); let current = guard.current(); (current.min_size, current.max_size) }); toplevel.with_pending_state(|state| { if state.states.contains(xdg_toplevel::State::Fullscreen) { state.size = Some(self.view_size.to_i32_round()); } else { let size = self.new_window_size(width, height, is_floating, rules, (min_size, max_size)); state.size = Some(size); } if is_floating { state.bounds = Some(self.floating.new_window_toplevel_bounds(rules)); } else { state.bounds = Some(self.scrolling.new_window_toplevel_bounds(rules)); } }); } pub fn focus_left(&mut self) -> bool { if self.floating_is_active.get() { self.floating.focus_left() } else { self.scrolling.focus_left() } } pub fn focus_right(&mut self) -> bool { if self.floating_is_active.get() { self.floating.focus_right() } else { self.scrolling.focus_right() } } pub fn focus_column_first(&mut self) { if self.floating_is_active.get() { self.floating.focus_leftmost(); } else { self.scrolling.focus_column_first(); } } pub fn focus_column_last(&mut self) { if self.floating_is_active.get() { self.floating.focus_rightmost(); } else { self.scrolling.focus_column_last(); } } pub fn focus_column_right_or_first(&mut self) { if !self.focus_right() { self.focus_column_first(); } } pub fn focus_column_left_or_last(&mut self) { if !self.focus_left() { self.focus_column_last(); } } pub fn focus_column(&mut self, index: usize) { if self.floating_is_active.get() { self.focus_tiling(); } self.scrolling.focus_column(index); } pub fn focus_window_in_column(&mut self, index: u8) { if self.floating_is_active.get() { return; } self.scrolling.focus_window_in_column(index); } pub fn focus_down(&mut self) -> bool { if self.floating_is_active.get() { self.floating.focus_down() } else { self.scrolling.focus_down() } } pub fn focus_up(&mut self) -> bool { if self.floating_is_active.get() { self.floating.focus_up() } else { self.scrolling.focus_up() } } pub fn focus_down_or_left(&mut self) { if self.floating_is_active.get() { self.floating.focus_down(); } else { self.scrolling.focus_down_or_left(); } } pub fn focus_down_or_right(&mut self) { if self.floating_is_active.get() { self.floating.focus_down(); } else { self.scrolling.focus_down_or_right(); } } pub fn focus_up_or_left(&mut self) { if self.floating_is_active.get() { self.floating.focus_up(); } else { self.scrolling.focus_up_or_left(); } } pub fn focus_up_or_right(&mut self) { if self.floating_is_active.get() { self.floating.focus_up(); } else { self.scrolling.focus_up_or_right(); } } pub fn focus_window_top(&mut self) { if self.floating_is_active.get() { self.floating.focus_topmost(); } else { self.scrolling.focus_top(); } } pub fn focus_window_bottom(&mut self) { if self.floating_is_active.get() { self.floating.focus_bottommost(); } else { self.scrolling.focus_bottom(); } } pub fn focus_window_down_or_top(&mut self) { if !self.focus_down() { self.focus_window_top(); } } pub fn focus_window_up_or_bottom(&mut self) { if !self.focus_up() { self.focus_window_bottom(); } } pub fn move_left(&mut self) -> bool { if self.floating_is_active.get() { self.floating.move_left(); true } else { self.scrolling.move_left() } } pub fn move_right(&mut self) -> bool { if self.floating_is_active.get() { self.floating.move_right(); true } else { self.scrolling.move_right() } } pub fn move_column_to_first(&mut self) { if self.floating_is_active.get() { return; } self.scrolling.move_column_to_first(); } pub fn move_column_to_last(&mut self) { if self.floating_is_active.get() { return; } self.scrolling.move_column_to_last(); } pub fn move_column_to_index(&mut self, index: usize) { if self.floating_is_active.get() { return; } self.scrolling.move_column_to_index(index); } pub fn move_down(&mut self) -> bool { if self.floating_is_active.get() { self.floating.move_down(); true } else { self.scrolling.move_down() } } pub fn move_up(&mut self) -> bool { if self.floating_is_active.get() { self.floating.move_up(); true } else { self.scrolling.move_up() } } pub fn consume_or_expel_window_left(&mut self, window: Option<&W::Id>) { if window.map_or(self.floating_is_active.get(), |id| { self.floating.has_window(id) }) { return; } self.scrolling.consume_or_expel_window_left(window); } pub fn consume_or_expel_window_right(&mut self, window: Option<&W::Id>) { if window.map_or(self.floating_is_active.get(), |id| { self.floating.has_window(id) }) { return; } self.scrolling.consume_or_expel_window_right(window); } pub fn consume_into_column(&mut self) { if self.floating_is_active.get() { return; } self.scrolling.consume_into_column(); } pub fn expel_from_column(&mut self) { if self.floating_is_active.get() { return; } self.scrolling.expel_from_column(); } pub fn swap_window_in_direction(&mut self, direction: ScrollDirection) { if self.floating_is_active.get() { return; } self.scrolling.swap_window_in_direction(direction); } pub fn toggle_column_tabbed_display(&mut self) { if self.floating_is_active.get() { return; } self.scrolling.toggle_column_tabbed_display(); } pub fn set_column_display(&mut self, display: ColumnDisplay) { if self.floating_is_active.get() { return; } self.scrolling.set_column_display(display); } pub fn center_column(&mut self) { if self.floating_is_active.get() { self.floating.center_window(None); } else { self.scrolling.center_column(); } } pub fn center_window(&mut self, id: Option<&W::Id>) { if id.map_or(self.floating_is_active.get(), |id| { self.floating.has_window(id) }) { self.floating.center_window(id); } else { self.scrolling.center_window(id); } } pub fn toggle_width(&mut self) { if self.floating_is_active.get() { self.floating.toggle_window_width(None); } else { self.scrolling.toggle_width(); } } pub fn toggle_full_width(&mut self) { if self.floating_is_active.get() { // Leave this unimplemented for now. For good UX, this probably needs moving the tile // to be against the left edge of the working area while it is full-width. return; } self.scrolling.toggle_full_width(); } pub fn set_column_width(&mut self, change: SizeChange) { if self.floating_is_active.get() { self.floating.set_window_width(None, change, true); } else { self.scrolling.set_window_width(None, change); } } pub fn set_window_width(&mut self, window: Option<&W::Id>, change: SizeChange) { if window.map_or(self.floating_is_active.get(), |id| { self.floating.has_window(id) }) { self.floating.set_window_width(window, change, true); } else { self.scrolling.set_window_width(window, change); } } pub fn set_window_height(&mut self, window: Option<&W::Id>, change: SizeChange) { if window.map_or(self.floating_is_active.get(), |id| { self.floating.has_window(id) }) { self.floating.set_window_height(window, change, true); } else { self.scrolling.set_window_height(window, change); } } pub fn reset_window_height(&mut self, window: Option<&W::Id>) { if window.map_or(self.floating_is_active.get(), |id| { self.floating.has_window(id) }) { return; } self.scrolling.reset_window_height(window); } pub fn toggle_window_width(&mut self, window: Option<&W::Id>) { if window.map_or(self.floating_is_active.get(), |id| { self.floating.has_window(id) }) { self.floating.toggle_window_width(window); } else { self.scrolling.toggle_window_width(window); } } pub fn toggle_window_height(&mut self, window: Option<&W::Id>) { if window.map_or(self.floating_is_active.get(), |id| { self.floating.has_window(id) }) { self.floating.toggle_window_height(window); } else { self.scrolling.toggle_window_height(window); } } pub fn expand_column_to_available_width(&mut self) { if self.floating_is_active.get() { return; } self.scrolling.expand_column_to_available_width(); } pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) { let mut unfullscreen_to_floating = false; if self.floating.has_window(window) { if is_fullscreen { unfullscreen_to_floating = true; self.toggle_window_floating(Some(window)); } else { // Floating windows are never fullscreen, so this is an unfullscreen request for an // already unfullscreen window. return; } } else if !is_fullscreen { // The window is in the scrolling layout and we're requesting an unfullscreen. If it is // indeed fullscreen (i.e. this isn't a duplicate unfullscreen request), then we may // need to unfullscreen into floating. let tile = self .scrolling .tiles() .find(|tile| tile.window().id() == window) .unwrap(); if tile.window().is_pending_fullscreen() && tile.unfullscreen_to_floating { // Unfullscreen and float in one call so it has a chance to notice and request a // (0, 0) size, rather than the scrolling column size. self.toggle_window_floating(Some(window)); return; } } let changed = self.scrolling.set_fullscreen(window, is_fullscreen); // When going to fullscreen, remember if we should unfullscreen to floating. if changed && is_fullscreen { let tile = self .scrolling .tiles_mut() .find(|tile| tile.window().id() == window) .unwrap(); tile.unfullscreen_to_floating = unfullscreen_to_floating; } } pub fn toggle_fullscreen(&mut self, window: &W::Id) { let tile = self .tiles() .find(|tile| tile.window().id() == window) .unwrap(); let current = tile.window().is_pending_fullscreen(); self.set_fullscreen(window, !current); } pub fn toggle_window_floating(&mut self, id: Option<&W::Id>) { let active_id = self.active_window().map(|win| win.id().clone()); let target_is_active = id.map_or(true, |id| Some(id) == active_id.as_ref()); let Some(id) = id.cloned().or(active_id) else { return; }; let (_, render_pos, _) = self .tiles_with_render_positions() .find(|(tile, _, _)| *tile.window().id() == id) .unwrap(); if self.floating.has_window(&id) { let removed = self.floating.remove_tile(&id); // FIXME: compute closest pos? self.scrolling.add_tile( None, removed.tile, target_is_active, removed.width, removed.is_full_width, None, ); if target_is_active { self.floating_is_active = FloatingActive::No; } } else { let mut removed = self.scrolling.remove_tile(&id, Transaction::new()); removed.tile.stop_move_animations(); // Come up with a default floating position close to the tile position. let stored_or_default = self.floating.stored_or_default_tile_pos(&removed.tile); if stored_or_default.is_none() { let offset = if self.options.center_focused_column == CenterFocusedColumn::Always { Point::from((0., 0.)) } else { Point::from((50., 50.)) }; let pos = render_pos + offset; let size = removed.tile.tile_size(); let pos = self.floating.clamp_within_working_area(pos, size); let pos = self.floating.logical_to_size_frac(pos); removed.tile.floating_pos = Some(pos); } self.floating.add_tile(removed.tile, target_is_active); if target_is_active { self.floating_is_active = FloatingActive::Yes; } } let (tile, new_render_pos) = self .tiles_with_render_positions_mut(false) .find(|(tile, _)| *tile.window().id() == id) .unwrap(); tile.animate_move_from(render_pos - new_render_pos); } pub fn set_window_floating(&mut self, id: Option<&W::Id>, floating: bool) { if id.map_or(self.floating_is_active.get(), |id| { self.floating.has_window(id) }) == floating { return; } self.toggle_window_floating(id); } pub fn focus_floating(&mut self) { if !self.floating_is_active.get() { self.switch_focus_floating_tiling(); } } pub fn focus_tiling(&mut self) { if self.floating_is_active.get() { self.switch_focus_floating_tiling(); } } pub fn switch_focus_floating_tiling(&mut self) { if self.floating.is_empty() { // If floating is empty, keep focus on scrolling. return; } else if self.scrolling.is_empty() { // If floating isn't empty but scrolling is, keep focus on floating. return; } self.floating_is_active = if self.floating_is_active.get() { FloatingActive::No } else { FloatingActive::Yes }; } pub fn move_floating_window( &mut self, id: Option<&W::Id>, x: PositionChange, y: PositionChange, animate: bool, ) { if id.map_or(self.floating_is_active.get(), |id| { self.floating.has_window(id) }) { self.floating.move_window(id, x, y, animate); } else { // If the target tile isn't floating, set its stored floating position. let tile = if let Some(id) = id { self.scrolling .tiles_mut() .find(|tile| tile.window().id() == id) .unwrap() } else if let Some(tile) = self.scrolling.active_tile_mut() { tile } else { return; }; let working_area_loc = self.floating.working_area().loc; let pos = self.floating.stored_or_default_tile_pos(tile); // If there's no stored floating position, we can only set both components at once, not // adjust. let pos = pos.or_else(|| { (matches!(x, PositionChange::SetFixed(_)) && matches!(y, PositionChange::SetFixed(_))) .then_some(Point::default()) }); let Some(mut pos) = pos else { return; }; match x { PositionChange::SetFixed(x) => pos.x = x + working_area_loc.x, PositionChange::AdjustFixed(x) => pos.x += x, } match y { PositionChange::SetFixed(y) => pos.y = y + working_area_loc.y, PositionChange::AdjustFixed(y) => pos.y += y, } let pos = self.floating.logical_to_size_frac(pos); tile.floating_pos = Some(pos); } } pub fn has_windows(&self) -> bool { self.windows().next().is_some() } pub fn has_window(&self, window: &W::Id) -> bool { self.windows().any(|win| win.id() == window) } pub fn find_wl_surface(&self, wl_surface: &WlSurface) -> Option<&W> { self.windows().find(|win| win.is_wl_surface(wl_surface)) } pub fn find_wl_surface_mut(&mut self, wl_surface: &WlSurface) -> Option<&mut W> { self.windows_mut().find(|win| win.is_wl_surface(wl_surface)) } pub fn tiles_with_render_positions( &self, ) -> impl Iterator, Point, bool)> { let scrolling = self.scrolling.tiles_with_render_positions(); let floating = self.floating.tiles_with_render_positions(); let visible = self.is_floating_visible(); let floating = floating.map(move |(tile, pos)| (tile, pos, visible)); floating.chain(scrolling) } pub fn tiles_with_render_positions_mut( &mut self, round: bool, ) -> impl Iterator, Point)> { let scrolling = self.scrolling.tiles_with_render_positions_mut(round); let floating = self.floating.tiles_with_render_positions_mut(round); floating.chain(scrolling) } pub fn active_tile_visual_rectangle(&self) -> Option> { if self.floating_is_active.get() { self.floating.active_tile_visual_rectangle() } else { self.scrolling.active_tile_visual_rectangle() } } pub fn popup_target_rect(&self, window: &W::Id) -> Option> { if self.floating.has_window(window) { self.floating.popup_target_rect(window) } else { self.scrolling.popup_target_rect(window) } } pub fn render_elements( &self, renderer: &mut R, target: RenderTarget, focus_ring: bool, ) -> ( impl Iterator>, impl Iterator>, ) { let scrolling_focus_ring = focus_ring && !self.floating_is_active(); let scrolling = self .scrolling .render_elements(renderer, target, scrolling_focus_ring); let scrolling = scrolling.into_iter().map(WorkspaceRenderElement::from); let floating_focus_ring = focus_ring && self.floating_is_active(); let floating = self.is_floating_visible().then(|| { let view_rect = Rectangle::from_size(self.view_size); let floating = self.floating .render_elements(renderer, view_rect, target, floating_focus_ring); floating.into_iter().map(WorkspaceRenderElement::from) }); let floating = floating.into_iter().flatten(); (floating, scrolling) } pub fn render_shadow( &self, renderer: &mut R, ) -> impl Iterator + '_ { self.shadow.render(renderer, Point::from((0., 0.))) } pub fn render_above_top_layer(&self) -> bool { self.scrolling.render_above_top_layer() } pub fn is_floating_visible(&self) -> bool { // If the focus is on a fullscreen scrolling window, hide the floating windows. matches!( self.floating_is_active, FloatingActive::Yes | FloatingActive::NoButRaised ) || !self.render_above_top_layer() } pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer, window: &W::Id) { let view_size = self.view_size(); for (tile, tile_pos) in self.tiles_with_render_positions_mut(false) { 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.store_unmap_snapshot_if_empty(renderer); return; } } } pub fn clear_unmap_snapshot(&mut self, window: &W::Id) { for tile in self.tiles_mut() { if tile.window().id() == window { let _ = tile.take_unmap_snapshot(); return; } } } pub fn start_close_animation_for_window( &mut self, renderer: &mut GlesRenderer, window: &W::Id, blocker: TransactionBlocker, ) { if self.floating.has_window(window) { self.floating .start_close_animation_for_window(renderer, window, blocker); } else { self.scrolling .start_close_animation_for_window(renderer, window, blocker); } } pub fn start_close_animation_for_tile( &mut self, renderer: &mut GlesRenderer, snapshot: TileRenderSnapshot, tile_size: Size, tile_pos: Point, blocker: TransactionBlocker, ) { self.floating .start_close_animation_for_tile(renderer, snapshot, tile_size, tile_pos, blocker); } pub fn start_open_animation(&mut self, id: &W::Id) -> bool { self.scrolling.start_open_animation(id) || self.floating.start_open_animation(id) } pub fn window_under(&self, pos: Point) -> Option<(&W, HitType)> { // This logic is consistent with tiles_with_render_positions(). if self.is_floating_visible() { if let Some(rv) = self .floating .tiles_with_render_positions() .find_map(|(tile, tile_pos)| HitType::hit_tile(tile, tile_pos, pos)) { return Some(rv); } } self.scrolling.window_under(pos) } pub fn resize_edges_under(&self, pos: Point) -> Option { self.tiles_with_render_positions() .find_map(|(tile, tile_pos, visible)| { // This logic should be consistent with window_under() in when it returns Some vs. // None. if !visible { return None; } let pos_within_tile = pos - tile_pos; if tile.hit(pos_within_tile).is_some() { let size = tile.tile_size().to_f64(); let mut edges = ResizeEdge::empty(); if pos_within_tile.x < size.w / 3. { edges |= ResizeEdge::LEFT; } else if 2. * size.w / 3. < pos_within_tile.x { edges |= ResizeEdge::RIGHT; } if pos_within_tile.y < size.h / 3. { edges |= ResizeEdge::TOP; } else if 2. * size.h / 3. < pos_within_tile.y { edges |= ResizeEdge::BOTTOM; } return Some(edges); } None }) } pub fn descendants_added(&mut self, id: &W::Id) -> bool { self.floating.descendants_added(id) } pub fn update_window(&mut self, window: &W::Id, serial: Option) { if !self.floating.update_window(window, serial) { self.scrolling.update_window(window, serial); } } pub fn refresh(&mut self, is_active: bool) { self.scrolling .refresh(is_active && !self.floating_is_active.get()); self.floating .refresh(is_active && self.floating_is_active.get()); } pub fn scroll_amount_to_activate(&self, window: &W::Id) -> f64 { if self.floating.has_window(window) { return 0.; } self.scrolling.scroll_amount_to_activate(window) } pub fn activate_window(&mut self, window: &W::Id) -> bool { if self.floating.activate_window(window) { self.floating_is_active = FloatingActive::Yes; true } else if self.scrolling.activate_window(window) { self.floating_is_active = FloatingActive::No; true } else { false } } pub fn activate_window_without_raising(&mut self, window: &W::Id) -> bool { if self.floating.activate_window_without_raising(window) { self.floating_is_active = FloatingActive::Yes; true } else if self.scrolling.activate_window(window) { self.floating_is_active = match self.floating_is_active { FloatingActive::No => FloatingActive::No, FloatingActive::NoButRaised => FloatingActive::NoButRaised, FloatingActive::Yes => FloatingActive::NoButRaised, }; true } else { false } } pub(super) fn scrolling_insert_position(&self, pos: Point) -> InsertPosition { self.scrolling.insert_position(pos) } pub(super) fn insert_hint_area( &self, position: InsertPosition, ) -> Option> { self.scrolling.insert_hint_area(position) } pub fn view_offset_gesture_begin(&mut self, is_touchpad: bool) { self.scrolling.view_offset_gesture_begin(is_touchpad); } pub fn view_offset_gesture_update( &mut self, delta_x: f64, timestamp: Duration, is_touchpad: bool, ) -> Option { self.scrolling .view_offset_gesture_update(delta_x, timestamp, is_touchpad) } pub fn view_offset_gesture_end(&mut self, is_touchpad: Option) -> bool { self.scrolling.view_offset_gesture_end(is_touchpad) } pub fn dnd_scroll_gesture_begin(&mut self) { self.scrolling.dnd_scroll_gesture_begin(); } pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point, speed: f64) -> bool { let config = &self.options.gestures.dnd_edge_view_scroll; let trigger_width = config.trigger_width.0; // This working area intentionally does not include extra struts from Options. let x = pos.x - self.working_area.loc.x; let width = self.working_area.size.w; let x = x.clamp(0., width); let trigger_width = trigger_width.clamp(0., width / 2.); let delta = if x < trigger_width { -(trigger_width - x) } else if width - x < trigger_width { trigger_width - (width - x) } else { 0. }; let delta = if trigger_width < 0.01 { // Sanity check for trigger-width 0 or small window sizes. 0. } else { // Normalize to [0, 1]. delta / trigger_width }; let delta = delta * speed; self.scrolling.dnd_scroll_gesture_scroll(delta) } pub fn dnd_scroll_gesture_end(&mut self) { self.scrolling.dnd_scroll_gesture_end(); } pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool { if self.floating.has_window(&window) { self.floating.interactive_resize_begin(window, edges) } else { self.scrolling.interactive_resize_begin(window, edges) } } pub fn interactive_resize_update( &mut self, window: &W::Id, delta: Point, ) -> bool { if self.floating.has_window(window) { self.floating.interactive_resize_update(window, delta) } else { self.scrolling.interactive_resize_update(window, delta) } } pub fn interactive_resize_end(&mut self, window: Option<&W::Id>) { if let Some(window) = window { if self.floating.has_window(window) { self.floating.interactive_resize_end(Some(window)); } else { self.scrolling.interactive_resize_end(Some(window)); } } else { self.floating.interactive_resize_end(None); self.scrolling.interactive_resize_end(None); } } pub fn floating_is_active(&self) -> bool { self.floating_is_active.get() } pub fn floating_logical_to_size_frac( &self, logical_pos: Point, ) -> Point { self.floating.logical_to_size_frac(logical_pos) } pub fn working_area(&self) -> Rectangle { self.working_area } #[cfg(test)] pub fn scrolling(&self) -> &ScrollingSpace { &self.scrolling } #[cfg(test)] pub fn floating(&self) -> &FloatingSpace { &self.floating } #[cfg(test)] pub fn verify_invariants(&self, move_win_id: Option<&W::Id>) { use approx::assert_abs_diff_eq; let scale = self.scale.fractional_scale(); assert!(self.view_size.w > 0.); assert!(self.view_size.h > 0.); assert!(scale > 0.); assert!(scale.is_finite()); assert_eq!(self.view_size, self.scrolling.view_size()); assert_eq!(&self.clock, self.scrolling.clock()); assert!(Rc::ptr_eq(&self.options, self.scrolling.options())); self.scrolling.verify_invariants(self.working_area); assert_eq!(self.view_size, self.floating.view_size()); assert_eq!(self.working_area, self.floating.working_area()); assert_eq!(&self.clock, self.floating.clock()); assert!(Rc::ptr_eq(&self.options, self.floating.options())); self.floating.verify_invariants(); if self.floating.is_empty() { assert!( !self.floating_is_active.get(), "when floating is empty it must never be active" ); } else if self.scrolling.is_empty() { assert!( self.floating_is_active.get(), "when scrolling is empty but floating isn't, floating should be active" ); } for (tile, tile_pos, visible) in self.tiles_with_render_positions() { if Some(tile.window().id()) != move_win_id { assert_eq!(tile.interactive_move_offset, Point::from((0., 0.))); } let rounded_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale); // Tile positions must be rounded to physical pixels. assert_abs_diff_eq!(tile_pos.x, rounded_pos.x, epsilon = 1e-5); assert_abs_diff_eq!(tile_pos.y, rounded_pos.y, epsilon = 1e-5); if let Some(alpha) = &tile.alpha_animation { let anim = &alpha.anim; if visible { assert_eq!(anim.to(), 1., "visible tiles can animate alpha only to 1"); } assert!( !alpha.hold_after_done, "tiles in the layout cannot have held alpha animation" ); } } } } pub(super) fn compute_working_area(output: &Output) -> Rectangle { layer_map_for_output(output).non_exclusive_zone().to_f64() } fn compute_workspace_shadow_config( config: niri_config::WorkspaceShadow, view_size: Size, ) -> niri_config::Shadow { // Gaps between workspaces are a multiple of the view height, so shadow settings should also be // normalized to the view height to prevent them from overlapping on lower resolutions. let norm = view_size.h / 1080.; let mut config = niri_config::Shadow::from(config); config.softness.0 *= norm; config.spread.0 *= norm; config.offset.x.0 *= norm; config.offset.y.0 *= norm; config }