diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 0b1250e..d9f0127 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -773,7 +773,13 @@ fn lid_hides_panel(closed: bool, external_available: bool) -> bool { closed && external_available } -fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: crate::frame_scheduler::FramePlan) { +// Return the sampled scene's activity so dispatch does not rescan the windows. +fn render_output( + state: &mut Ferese, + node: DrmNode, + crtc: crtc::Handle, + plan: crate::frame_scheduler::FramePlan, +) -> bool { if state.session_lock.active && state.session_lock.sleeping { sleep_locked_outputs(state); let asleep = state @@ -783,7 +789,7 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr .and_then(|device| device.outputs.get(&crtc)) .is_none_or(|output| output.power_off || output.frame_pending); if asleep { - return; + return false; } } @@ -798,18 +804,18 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr .as_mut() .and_then(|backend| backend.devices.remove(&node)) else { - return; + return false; }; let Some(mut output) = device.outputs.remove(&crtc) else { restore_device(state, node, device); - return; + return false; }; if !device.drm.is_active() || output.frame_pending { device.outputs.insert(crtc, output); restore_device(state, node, device); - return; + return false; } let render_started = Instant::now(); @@ -826,7 +832,7 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale()); let flags = planes::frame_flags( state.output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting()), - state.output_has_animations(&output.output), + frame.animating, state.session_lock.active, ); let result = @@ -919,6 +925,8 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr if !submitted { schedule_frame_callbacks(state, node, crtc, plan.present_at); } + + frame.animating } fn restore_device(state: &mut Ferese, node: DrmNode, device: DirectDevice) { diff --git a/compositor/src/backends/direct/scheduling.rs b/compositor/src/backends/direct/scheduling.rs index cb1f0fa..64621fb 100644 --- a/compositor/src/backends/direct/scheduling.rs +++ b/compositor/src/backends/direct/scheduling.rs @@ -120,8 +120,9 @@ fn dispatch_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identit backend.animation_active = animating; } - render_output(state, node, crtc, plan); - if state.output_has_animations(identity) { + let output_animating = render_output(state, node, crtc, plan); + + if output_animating { request_frame(state, node, crtc); } diff --git a/compositor/src/config.rs b/compositor/src/config.rs index 0102c60..8b77aa8 100644 --- a/compositor/src/config.rs +++ b/compositor/src/config.rs @@ -11,9 +11,9 @@ use std::path::PathBuf; use appearance::*; pub use appearance::{BorderGradient, InactiveDimSettings, MaterialStyle, ThemeSettings}; -pub(crate) use bindings::physical_keymap; use bindings::*; pub use bindings::{Binding, BindingAction}; +pub(crate) use bindings::{BindingSet, physical_keymap}; use ferese_animation::SpringConfig; use ferese_core::LayoutMode; use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy}; @@ -939,18 +939,19 @@ mod tests { assert!(bindings.iter().any(|binding| { binding.modifiers.logo && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Return) - && binding.action == BindingAction::Spawn(vec!["foot".to_owned()]) + && binding.action == BindingAction::Spawn(vec!["foot".to_owned()].into()) })); assert!(bindings.iter().any(|binding| { binding.modifiers.logo && binding.modifiers.shift && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_s) - && binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned()]) + && binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned()].into()) })); assert!(bindings.iter().any(|binding| { binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Print) && binding.modifiers == BindingModifiers::default() - && binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned(), "--full".to_owned()]) + && binding.action + == BindingAction::Spawn(vec!["ferese-screenshot".to_owned(), "--full".to_owned()].into()) })); assert!(bindings.iter().any(|binding| { binding.modifiers.logo @@ -970,7 +971,8 @@ mod tests { let bindings = replaced.bindings(&input).unwrap(); assert_eq!(bindings.len(), 53); assert!(bindings.iter().any(|binding| { - binding.action == BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()]) + binding.action + == BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()].into()) })); let input = unbound.input_settings().unwrap(); diff --git a/compositor/src/config/bindings.rs b/compositor/src/config/bindings.rs index 37727e8..3aa2389 100644 --- a/compositor/src/config/bindings.rs +++ b/compositor/src/config/bindings.rs @@ -1,5 +1,10 @@ +use std::sync::Arc; + use super::*; +mod index; +pub(crate) use index::BindingSet; + impl Config { pub fn bindings(&self, input: &InputSettings) -> Result, ConfigError> { let mut commands = HashMap::from([ @@ -280,7 +285,7 @@ pub(super) fn parse_action( let argv = commands .get(command) .ok_or_else(|| ConfigError::Binding(format!("unknown command {command:?}")))?; - Ok(BindingAction::Spawn(argv.clone())) + Ok(BindingAction::Spawn(argv.clone().into())) } "close" => { no_argument()?; @@ -440,7 +445,7 @@ pub(super) struct BindingModifiers { #[derive(Clone, Debug, PartialEq)] pub enum BindingAction { None, - Spawn(Vec), + Spawn(Arc<[String]>), Close, Exit, Focus(ferese_layout::Direction), diff --git a/compositor/src/config/bindings/index.rs b/compositor/src/config/bindings/index.rs new file mode 100644 index 0000000..a298bf6 --- /dev/null +++ b/compositor/src/config/bindings/index.rs @@ -0,0 +1,179 @@ +use super::*; + +/// Immutable bindings and their keyboard index are replaced together on reload. +/// The ordered slice remains available for gestures and the shortcut guide. +pub(crate) struct BindingSet { + ordered: Vec, + keyboard: HashMap, +} + +impl From> for BindingSet { + fn from(ordered: Vec) -> Self { + let mut keyboard = HashMap::with_capacity(ordered.len()); + + for (index, binding) in ordered.iter().enumerate() { + let identity = binding_identity_for_runtime(binding); + + if identity.match_mode != BindingMatch::Swipe { + keyboard.entry(identity).or_insert(index); + } + } + + Self { ordered, keyboard } + } +} + +impl std::ops::Deref for BindingSet { + type Target = [Binding]; + + fn deref(&self) -> &Self::Target { + &self.ordered + } +} + +impl BindingSet { + pub(crate) fn action_for_key( + &self, + keycode: Keycode, + symbols: &[u32], + logo: bool, + ctrl: bool, + alt: bool, + shift: bool, + ) -> Option<&BindingAction> { + let modifiers = BindingModifiers { logo, ctrl, alt, shift }; + let physical = self.keyboard.get(&BindingIdentity { + modifiers, + match_mode: BindingMatch::Physical, + key: keycode.raw(), + }); + let symbolic = symbols.iter().filter_map(|&key| { + self.keyboard.get(&BindingIdentity { + modifiers, + match_mode: BindingMatch::Keysym, + key, + }) + }); + // Preserve list precedence across physical keys and all raw symbols; + // neither hash iteration order nor symbol order determines the winner. + physical + .into_iter() + .chain(symbolic) + .min() + .map(|&index| &self.ordered[index].action) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn binding_for(trigger: BindingTrigger, action: BindingAction) -> Binding { + Binding { + modifiers: BindingModifiers::default(), + trigger, + action, + } + } + + #[test] + fn keyboard_index_preserves_order_across_symbols_and_physical_keys() { + let code = Keycode::new(38); + let bindings = vec![ + binding_for(BindingTrigger::Keysym(keysyms::KEY_b), BindingAction::Close), + binding_for(BindingTrigger::Physical(code), BindingAction::ToggleFloating), + binding_for(BindingTrigger::Keysym(keysyms::KEY_a), BindingAction::Exit), + binding_for(BindingTrigger::Keysym(keysyms::KEY_b), BindingAction::None), + ]; + + for ordered in [bindings.clone(), bindings.into_iter().rev().collect()] { + let index = BindingSet::from(ordered); + + for symbols in [ + vec![keysyms::KEY_a, keysyms::KEY_b], + vec![keysyms::KEY_b, keysyms::KEY_a], + vec![], + ] { + let expected = index + .iter() + .find(|binding| binding.matches(code, &symbols, false, false, false, false)); + assert_eq!( + index.action_for_key(code, &symbols, false, false, false, false), + expected.map(|binding| &binding.action) + ); + } + } + } + + #[test] + fn indexed_lookup_matches_linear_lookup_for_all_modifier_combinations() { + let config = Config::default(); + let mut bindings = config.bindings(&config.input_settings().unwrap()).unwrap(); + bindings.extend([ + binding_for(BindingTrigger::Physical(Keycode::new(38)), BindingAction::None), + binding_for(BindingTrigger::Keysym(keysyms::KEY_a), BindingAction::Close), + binding_for( + BindingTrigger::Swipe { + fingers: 3, + direction: crate::gestures::SwipeDirection::Left, + }, + BindingAction::Focus(Direction::Left), + ), + ]); + let index = BindingSet::from(bindings); + + for bits in 0..16 { + let (logo, ctrl, alt, shift) = (bits & 1 != 0, bits & 2 != 0, bits & 4 != 0, bits & 8 != 0); + + for code in 8..100 { + for symbols in [ + vec![], + vec![keysyms::KEY_Return], + vec![keysyms::KEY_b, keysyms::KEY_a], + vec![keysyms::KEY_a, keysyms::KEY_Tab], + vec![keysyms::KEY_NoSymbol], + ] { + let code = Keycode::new(code); + let expected = index + .iter() + .find(|binding| binding.matches(code, &symbols, logo, ctrl, alt, shift)); + assert_eq!( + index.action_for_key(code, &symbols, logo, ctrl, alt, shift), + expected.map(|binding| &binding.action) + ); + } + } + } + } + + #[test] + fn replacing_bindings_discards_old_index_entries() { + let code = Keycode::new(38); + let mut bindings = BindingSet::from(vec![binding_for(BindingTrigger::Physical(code), BindingAction::Close)]); + assert_eq!( + bindings.action_for_key(code, &[], false, false, false, false), + Some(&BindingAction::Close) + ); + bindings = BindingSet::from(vec![binding_for( + BindingTrigger::Keysym(keysyms::KEY_b), + BindingAction::None, + )]); + assert_eq!(bindings.action_for_key(code, &[], false, false, false, false), None); + assert_eq!( + bindings.action_for_key(code, &[keysyms::KEY_b], false, false, false, false), + Some(&BindingAction::None) + ); + } + + #[test] + fn cloning_spawn_actions_shares_the_command_storage() { + let action = BindingAction::Spawn(vec!["foot".into(), "--app-id".into(), "work".into()].into()); + let cloned = action.clone(); + let (BindingAction::Spawn(original), BindingAction::Spawn(cloned)) = (&action, cloned) else { + unreachable!(); + }; + assert!(Arc::ptr_eq(original, &cloned)); + assert_eq!(original[0], "foot"); + assert_eq!(original.len(), 3); + } +} diff --git a/compositor/src/dimming.rs b/compositor/src/dimming.rs index 382a79e..e99b807 100644 --- a/compositor/src/dimming.rs +++ b/compositor/src/dimming.rs @@ -27,6 +27,10 @@ pub(crate) struct DimAnimation { } impl DimAnimation { + pub(crate) fn needs_update(&self, target: f64) -> bool { + self.target != target || self.current != target + } + pub(crate) fn is_animating(&self) -> bool { self.current != self.target } diff --git a/compositor/src/handlers/compositor.rs b/compositor/src/handlers/compositor.rs index b8a6338..9918efc 100644 --- a/compositor/src/handlers/compositor.rs +++ b/compositor/src/handlers/compositor.rs @@ -84,6 +84,7 @@ impl CompositorHandler for Ferese { } else if self.windows.ids().contains_key(&window) { self.remove_tiled_window(&window); self.space.map_element(window.clone(), (0, 0), false); + self.invalidate_window_stacking(); self.restore_keyboard_focus(); } if window diff --git a/compositor/src/handlers/mod.rs b/compositor/src/handlers/mod.rs index 5482a63..d36a1c2 100644 --- a/compositor/src/handlers/mod.rs +++ b/compositor/src/handlers/mod.rs @@ -59,14 +59,7 @@ impl SeatHandler for Ferese { } fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { - if let Some(id) = focused.and_then(|surface| { - self.windows.ids().iter().find_map(|(window, id)| { - window - .toplevel() - .is_some_and(|toplevel| toplevel.wl_surface() == surface) - .then_some(*id) - }) - }) { + if let Some(id) = focused.and_then(|surface| self.windows.id_for_surface(surface)) { self.focus_history.record(id); } if let Some(inhibitor) = self.active_shortcuts_inhibitor.take() @@ -152,12 +145,7 @@ impl Ferese { { root = parent; } - let window = self.windows.ids().iter().find_map(|(window, id)| { - window - .toplevel() - .is_some_and(|toplevel| toplevel.wl_surface() == &root) - .then_some(*id) - }); + let window = self.windows.id_for_surface(&root); let output = self.space.outputs().find(|output| { window.is_some_and(|id| self.window_belongs_to_output(id, output)) || layer_map_for_output(output) diff --git a/compositor/src/handlers/xdg_shell.rs b/compositor/src/handlers/xdg_shell.rs index 532ccf1..a8f27cc 100644 --- a/compositor/src/handlers/xdg_shell.rs +++ b/compositor/src/handlers/xdg_shell.rs @@ -30,25 +30,38 @@ impl XdgShellHandler for Ferese { // discoverable, but do not mutate/focus the layout until its initial // commit supplies the metadata needed to resolve placement rules. self.space.map_element(window, (0, 0), false); + self.invalidate_window_stacking(); + } + + fn title_changed(&mut self, surface: ToplevelSurface) { + if self.windows.refresh_metadata(surface.wl_surface()) { + self.send_shell_snapshots(); + self.cursor_redraw_pending = true; + } + } + + fn app_id_changed(&mut self, surface: ToplevelSurface) { + if self.windows.refresh_metadata(surface.wl_surface()) { + self.send_shell_snapshots(); + self.cursor_redraw_pending = true; + } } fn parent_changed(&mut self, surface: ToplevelSurface) { let Some(parent) = surface.parent() else { return; }; - let parent = self.windows.ids().iter().find_map(|(window, id)| { - window - .toplevel() - .is_some_and(|toplevel| toplevel.wl_surface() == &parent) - .then_some(*id) - }); + let parent = self.windows.id_for_surface(&parent); let child = self - .space - .elements() - .find(|window| { - window - .toplevel() - .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) + .windows + .id_for_surface(surface.wl_surface()) + .and_then(|id| self.windows.window(id)) + .or_else(|| { + self.space.elements().find(|window| { + window + .toplevel() + .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) + }) }) .cloned(); @@ -61,13 +74,14 @@ impl XdgShellHandler for Ferese { // Hidden workspace windows are managed but unmapped from Space. let Some(window) = self .windows - .ids() - .keys() - .chain(self.space.elements()) - .find(|window| { - window - .toplevel() - .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) + .id_for_surface(surface.wl_surface()) + .and_then(|id| self.windows.window(id)) + .or_else(|| { + self.space.elements().find(|window| { + window + .toplevel() + .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) + }) }) .cloned() else { @@ -186,12 +200,7 @@ impl XdgShellHandler for Ferese { } fn maximize_request(&mut self, surface: ToplevelSurface) { - if let Some(id) = self.windows.ids().iter().find_map(|(window, id)| { - window - .toplevel() - .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) - .then_some(*id) - }) { + if let Some(id) = self.windows.id_for_surface(surface.wl_surface()) { self.set_window_maximized(id, true); } else { surface.with_pending_state(|state| state.states.set(ToplevelState::Maximized)); @@ -199,12 +208,7 @@ impl XdgShellHandler for Ferese { } fn unmaximize_request(&mut self, surface: ToplevelSurface) { - if let Some(id) = self.windows.ids().iter().find_map(|(window, id)| { - window - .toplevel() - .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) - .then_some(*id) - }) { + if let Some(id) = self.windows.id_for_surface(surface.wl_surface()) { self.set_window_maximized(id, false); } else { surface.with_pending_state(|state| state.states.unset(ToplevelState::Maximized)); @@ -216,12 +220,7 @@ impl XdgShellHandler for Ferese { surface: ToplevelSurface, _output: Option, ) { - if let Some(window) = self.windows.ids().iter().find_map(|(window, id)| { - window - .toplevel() - .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) - .then_some(*id) - }) { + if let Some(window) = self.windows.id_for_surface(surface.wl_surface()) { self.set_window_fullscreen(window, true); } else { surface.with_pending_state(|state| state.states.set(ToplevelState::Fullscreen)); @@ -229,12 +228,7 @@ impl XdgShellHandler for Ferese { } fn unfullscreen_request(&mut self, surface: ToplevelSurface) { - if let Some(window) = self.windows.ids().iter().find_map(|(window, id)| { - window - .toplevel() - .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) - .then_some(*id) - }) { + if let Some(window) = self.windows.id_for_surface(surface.wl_surface()) { self.set_window_fullscreen(window, false); } else { surface.with_pending_state(|state| state.states.unset(ToplevelState::Fullscreen)); @@ -391,14 +385,9 @@ pub fn apply_initial_window_rules(state: &mut Ferese, window: &Window) { }) }); if initial { - let parent = toplevel.parent().and_then(|parent| { - state.windows.ids().iter().find_map(|(candidate, id)| { - candidate - .toplevel() - .is_some_and(|surface| surface.wl_surface() == &parent) - .then_some(*id) - }) - }); + let parent = toplevel + .parent() + .and_then(|parent| state.windows.id_for_surface(&parent)); state.add_rule_placed_window(window.clone(), app_id.as_deref(), title.as_deref(), parent); } state.apply_initial_window_rules(window, app_id.as_deref(), title.as_deref(), transient); diff --git a/compositor/src/input.rs b/compositor/src/input.rs index ae9f134..d1ddb0f 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -364,18 +364,14 @@ impl Ferese { .into_iter() .map(|symbol| symbol.raw()) .collect::>(); - let action = data.bindings.iter().find_map(|binding| { - binding - .matches( - keycode, - &raw_symbols, - modifiers.logo, - modifiers.ctrl, - modifiers.alt, - modifiers.shift, - ) - .then(|| binding.action.clone()) - }); + let action = data.bindings.action_for_key( + keycode, + &raw_symbols, + modifiers.logo, + modifiers.ctrl, + modifiers.alt, + modifiers.shift, + ); if data.focus_cycle.is_some() && state == KeyState::Pressed && !matches!(action, Some(BindingAction::FocusMru(_))) @@ -405,6 +401,8 @@ impl Ferese { if state == KeyState::Pressed { data.intercepted_keys.insert(keycode); + let action = action.clone(); + if let BindingAction::FocusMru(reverse) = action { data.cycle_focus(reverse, modifiers.alt); } else { @@ -814,6 +812,8 @@ impl Ferese { return; } + let previous = self.focused_window; + if let Some(window) = self.window_under_visual(position) { let focused = self.windows.ids().get(&window).copied(); if let Some(focused) = focused { @@ -833,8 +833,21 @@ impl Ferese { } else { // Hover transfers keyboard focus without changing the persistent // stack: an exposed window must not cover the floats above it. - for mapped in self.space.elements() { - mapped.set_activated(mapped == &window); + if let Some(previous) = previous + .filter(|id| Some(*id) != focused) + .and_then(|id| self.windows.window(id)) + { + previous.set_activated(false); + + if let Some(toplevel) = previous.toplevel() { + toplevel.send_pending_configure(); + } + } + + window.set_activated(true); + + if let Some(toplevel) = window.toplevel() { + toplevel.send_pending_configure(); } } let surface = window @@ -844,16 +857,18 @@ impl Ferese { .clone(); keyboard.set_focus(self, Some(surface), serial); } else { + if let Some(previous) = previous.and_then(|id| self.windows.window(id)) { + previous.set_activated(false); + + if let Some(toplevel) = previous.toplevel() { + toplevel.send_pending_configure(); + } + } + self.focused_window = None; keyboard.set_focus(self, Option::::None, serial); } - self.space.elements().for_each(|window| { - if let Some(toplevel) = window.toplevel() { - toplevel.send_pending_configure(); - } - }); - if raise { self.relayout(); } else { @@ -987,11 +1002,11 @@ impl Ferese { fn execute_binding(&mut self, action: BindingAction) { match action { BindingAction::None => {} - BindingAction::Spawn(mut argv) => { - let program = argv.remove(0); + BindingAction::Spawn(argv) => { + let (program, args) = argv.split_first().expect("binding commands are validated"); - match Command::new(&program) - .args(argv) + match Command::new(program) + .args(args) .env("WAYLAND_DISPLAY", &self.socket_name) .env_remove("WAYLAND_SOCKET") .spawn() diff --git a/compositor/src/overview.rs b/compositor/src/overview.rs index 89bc921..4382b83 100644 --- a/compositor/src/overview.rs +++ b/compositor/src/overview.rs @@ -84,8 +84,17 @@ pub(crate) fn init_font_loader( Ok(()) } -type OverviewStateLabel = - HashMap<(String, u64, [u32; 4]), (MemoryRenderBuffer, smithay::utils::Size)>; +type LabelBuffer = (MemoryRenderBuffer, smithay::utils::Size); +type LabelStyle = (u64, [u32; 4]); +type OverviewStateLabel = HashMap>; + +#[derive(Debug)] +struct WindowLabel { + revision: u64, + limit: usize, + style: LabelStyle, + buffer: LabelBuffer, +} #[derive(Debug)] pub(crate) struct OverviewState { @@ -97,6 +106,8 @@ pub(crate) struct OverviewState { font_requests: Option, font: Option, labels: OverviewStateLabel, + window_labels: HashMap, + label_count: usize, } #[derive(Clone, Debug)] @@ -118,6 +129,8 @@ impl Default for OverviewState { font_requests: None, font: None, labels: HashMap::new(), + window_labels: HashMap::new(), + label_count: 0, } } } @@ -163,6 +176,17 @@ fn centered_row_start(strip: Rect, count: usize, card_width: f64) -> f64 { } impl OverviewState { + pub(crate) fn needs_tick(&self) -> bool { + self.motion.exit_transition.is_some() + || self.motion.opacity.current != self.motion.opacity.target + || self.motion.opacity.velocity != 0.0 + || self + .motion + .presentations + .values() + .any(|rect| rect.current != rect.target || rect.velocity != ferese_animation::RectVelocity::default()) + } + pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool { self.motion.is_animating(spring) } @@ -200,6 +224,8 @@ impl OverviewState { } self.font = font; self.labels.clear(); + self.window_labels.clear(); + self.label_count = 0; true } @@ -209,8 +235,8 @@ impl OverviewState { scale: f64, color: [f32; 4], ) -> Option<(MemoryRenderBuffer, smithay::utils::Size)> { - let key = (text.to_owned(), scale.to_bits(), color.map(f32::to_bits)); - if let Some(buffer) = self.labels.get(&key) { + let key = (scale.to_bits(), color.map(f32::to_bits)); + if let Some(buffer) = self.labels.get(text).and_then(|styles| styles.get(&key)) { return Some(buffer.clone()); } let font = self.font.as_ref()?; @@ -249,11 +275,16 @@ impl OverviewState { } let buffer = MemoryRenderBuffer::from_slice(&pixels, Fourcc::Abgr8888, (width, height), 1, Transform::Normal, None); - if self.labels.len() >= 128 { + if self.label_count >= 128 { self.labels.clear(); + self.label_count = 0; } let label = (buffer, (width, height).into()); - self.labels.insert(key, label.clone()); + self.labels + .entry(text.to_owned()) + .or_default() + .insert(key, label.clone()); + self.label_count += 1; Some(label) } @@ -430,32 +461,53 @@ impl Ferese { } } - pub(crate) fn overview_window_label( - &mut self, - window: &smithay::desktop::Window, - scale: f64, - width: f64, - ) -> Option<(MemoryRenderBuffer, smithay::utils::Size)> { - use smithay::wayland::compositor::with_states; - use smithay::wayland::shell::xdg::XdgToplevelSurfaceData; - let surface = window.toplevel()?.wl_surface(); - let title = with_states(surface, |states| { - let data = states.data_map.get::()?.lock().ok()?; - data.title - .clone() - .filter(|title| !title.trim().is_empty()) - .or_else(|| data.app_id.clone()) - }) - .unwrap_or_else(|| "Window".into()); - let limit = ((width - 16.0) / 8.0).clamp(1.0, 48.0) as usize; - let mut chars = title.chars().filter(|c| !c.is_control()); - let mut title: String = chars.by_ref().take(limit).collect(); - if chars.next().is_some() { - title.pop(); - title.push('…'); - } + pub(crate) fn retain_overview_labels(&mut self) { self.overview - .label(&title, scale, self.theme_settings.text_primary_color.0) + .window_labels + .retain(|id, _| self.windows.window(*id).is_some()); + } + + pub(crate) fn overview_window_label(&mut self, id: WindowId, scale: f64, width: f64) -> Option { + let record = self.windows.record(id)?; + let limit = ((width - 16.0) / 8.0).clamp(1.0, 48.0) as usize; + let color = self.theme_settings.text_primary_color.0; + let style = (scale.to_bits(), color.map(f32::to_bits)); + + if let Some(label) = self.overview.window_labels.get(&id) + && label.revision == record.metadata_revision + && label.limit == limit + && label.style == style + { + return Some(label.buffer.clone()); + } + + let title = if !record.title.trim().is_empty() { + &record.title + } else if !record.app_id.is_empty() { + &record.app_id + } else { + "Window" + }; + let mut chars = title.chars().filter(|c| !c.is_control()); + let mut text: String = chars.by_ref().take(limit).collect(); + + if chars.next().is_some() { + text.pop(); + text.push('…'); + } + + let revision = record.metadata_revision; + let buffer = self.overview.label(&text, scale, color)?; + self.overview.window_labels.insert( + id, + WindowLabel { + revision, + limit, + style, + buffer: buffer.clone(), + }, + ); + Some(buffer) } pub(crate) fn overview_workspace_label( @@ -608,9 +660,7 @@ impl Ferese { let capacity = strip_capacity(strip); let workspaces = self .output_workspaces - .workspace_views(&self.workspaces) - .into_iter() - .filter(|view| view.output == output_id) + .workspace_views_for_output(&self.workspaces, output_id) .collect::>(); let selected_index = workspaces .iter() @@ -904,6 +954,15 @@ fn directional_distance(direction: Direction, current: (f64, f64), candidate: (f mod tests { use super::*; + #[test] + fn tiny_overview_target_changes_still_get_a_settling_tick() { + let mut overview = OverviewState::default(); + assert!(!overview.needs_tick()); + overview.motion.opacity.set_target(0.0001); + assert!(!overview.is_animating(SpringConfig::default())); + assert!(overview.needs_tick()); + } + fn test_font() -> FontArc { FontArc::try_from_slice(include_bytes!("../../assets/fonts/Comfortaa-Regular.otf")).unwrap() } diff --git a/compositor/src/presentation.rs b/compositor/src/presentation.rs index 64aa5eb..5e5be02 100644 --- a/compositor/src/presentation.rs +++ b/compositor/src/presentation.rs @@ -5,7 +5,6 @@ use std::time::{Duration, Instant}; use smithay::backend::renderer::element::{ Element, Id, Kind, RenderElement, RenderElementPresentationState, RenderElementStates, }; -use smithay::backend::renderer::gles::element::PixelShaderElement; use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform}; use smithay::backend::renderer::utils::{CommitCounter, DamageSet}; use smithay::desktop::utils::OutputPresentationFeedback; @@ -15,6 +14,8 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform}; use smithay::wayland::compositor::SurfaceData; +use crate::render::SharedPixelShaderElement; + pub(crate) const HANDOFF: Duration = Duration::from_millis(80); pub(crate) const SNAPSHOT_BUDGET: usize = 64 * 1024 * 1024; @@ -164,7 +165,7 @@ pub(crate) fn advance_handoff( /// A shader canvas whose damage/paint destination uses the shared pixel edges. #[derive(Clone, Debug)] pub(crate) struct PhysicalShaderElement { - pub inner: PixelShaderElement, + pub inner: SharedPixelShaderElement, pub geometry: Rectangle, } diff --git a/compositor/src/render/decorations.rs b/compositor/src/render/decorations.rs index 8296ac0..a634d2a 100644 --- a/compositor/src/render/decorations.rs +++ b/compositor/src/render/decorations.rs @@ -130,7 +130,7 @@ pub(super) fn window_border_element( let context = renderer.context_id().erased(); let buffers = &mut resources.windows.entry(id).or_default().borders; if !buffers.contexts.contains_key(&context) { - let element = PixelShaderElement::new( + let element = SharedPixelShaderElement::new( programs.border.clone(), geometry, None, @@ -164,16 +164,16 @@ pub(super) fn window_border_element( pub(super) fn border_uniforms(parameters: &BorderParameters) -> Vec> { vec![ - Uniform::new("clip_rect", parameters.clip_rect).into_owned(), - Uniform::new("radius", parameters.radius).into_owned(), - Uniform::new("border_width", parameters.width).into_owned(), - Uniform::new("border_color", parameters.color).into_owned(), - Uniform::new("border_color_to", parameters.color_to).into_owned(), - Uniform::new("gradient_line", parameters.gradient_line).into_owned(), - Uniform::new("focus_color", parameters.focus_color).into_owned(), - Uniform::new("focus_color_to", parameters.focus_color_to).into_owned(), - Uniform::new("focus_gradient_line", parameters.focus_gradient_line).into_owned(), - Uniform::new("focus_mix", parameters.focus_mix).into_owned(), + Uniform::new("clip_rect", parameters.clip_rect), + Uniform::new("radius", parameters.radius), + Uniform::new("border_width", parameters.width), + Uniform::new("border_color", parameters.color), + Uniform::new("border_color_to", parameters.color_to), + Uniform::new("gradient_line", parameters.gradient_line), + Uniform::new("focus_color", parameters.focus_color), + Uniform::new("focus_color_to", parameters.focus_color_to), + Uniform::new("focus_gradient_line", parameters.focus_gradient_line), + Uniform::new("focus_mix", parameters.focus_mix), ] } @@ -213,12 +213,12 @@ pub(super) fn window_tint_element( }; let uniforms = |p: &BorderParameters| { vec![ - Uniform::new("visible_rect", p.clip_rect).into_owned(), - Uniform::new("material_radius", p.radius).into_owned(), - Uniform::new("tint", p.color).into_owned(), - Uniform::new("paint_mode", 0.0_f32).into_owned(), - Uniform::new("shadow_rect", p.clip_rect).into_owned(), - Uniform::new("shadow_values", [0.0_f32; 2]).into_owned(), + Uniform::new("visible_rect", p.clip_rect), + Uniform::new("material_radius", p.radius), + Uniform::new("tint", p.color), + Uniform::new("paint_mode", 0.0_f32), + Uniform::new("shadow_rect", p.clip_rect), + Uniform::new("shadow_values", [0.0_f32; 2]), ] }; let context = renderer.context_id().erased(); @@ -228,7 +228,7 @@ pub(super) fn window_tint_element( &mut resources.windows.entry(id).or_default().dim }; let cached = buffers.contexts.entry(context).or_insert_with(|| CachedBorder { - element: PixelShaderElement::new( + element: SharedPixelShaderElement::new( program.0, geometry, None, @@ -293,7 +293,7 @@ pub(super) fn window_shadow_element( let context = renderer.context_id().erased(); let buffers = &mut resources.windows.entry(id).or_default().shadow; if !buffers.contexts.contains_key(&context) { - let element = PixelShaderElement::new( + let element = SharedPixelShaderElement::new( programs.shadow.clone(), bounds, None, @@ -331,11 +331,11 @@ pub(super) fn window_shadow_element( pub(super) fn shadow_uniforms(parameters: &ShadowParameters) -> Vec> { vec![ - Uniform::new("shadow_rect", parameters.shadow_rect).into_owned(), - Uniform::new("radius", parameters.radius).into_owned(), - Uniform::new("blur", parameters.blur).into_owned(), - Uniform::new("opacity", parameters.opacity).into_owned(), - Uniform::new("shadow_color", parameters.color).into_owned(), + Uniform::new("shadow_rect", parameters.shadow_rect), + Uniform::new("radius", parameters.radius), + Uniform::new("blur", parameters.blur), + Uniform::new("opacity", parameters.opacity), + Uniform::new("shadow_color", parameters.color), ] } diff --git a/compositor/src/render/materials.rs b/compositor/src/render/materials.rs index a419295..db148d9 100644 --- a/compositor/src/render/materials.rs +++ b/compositor/src/render/materials.rs @@ -248,7 +248,7 @@ pub(super) fn material_element( capture.dirty.clone(), )) } else { - MaterialElement::Fill(PixelShaderElement::new( + MaterialElement::Fill(SharedPixelShaderElement::new( program.0.clone(), geometry, parameters.opaque.then(|| vec![Rectangle::from_size(geometry.size)]), @@ -264,7 +264,7 @@ pub(super) fn material_element( .or_insert_with(|| CachedMaterial { element: make_element(), parameters: parameters.clone(), - shadow: PixelShaderElement::new( + shadow: SharedPixelShaderElement::new( program.0.clone(), parameters.shadow_bounds, None, @@ -356,14 +356,14 @@ pub(super) fn blur_uniform_names() -> [UniformName<'static>; 8] { pub(super) fn blur_uniforms(p: &MaterialParameters) -> Vec> { vec![ - Uniform::new("visible_rect", p.visible_framebuffer).into_owned(), - Uniform::new("material_radius", p.radius).into_owned(), - Uniform::new("texture_size", [p.sample_framebuffer[2], p.sample_framebuffer[3]]).into_owned(), - Uniform::new("capture_origin", [p.sample_framebuffer[0], p.sample_framebuffer[1]]).into_owned(), - Uniform::new("blur_radius", p.blur).into_owned(), - Uniform::new("presentation_alpha", p.presentation_alpha).into_owned(), - Uniform::new("background_opacity", p.background_opacity).into_owned(), - Uniform::new("tint", p.tint).into_owned(), + Uniform::new("visible_rect", p.visible_framebuffer), + Uniform::new("material_radius", p.radius), + Uniform::new("texture_size", [p.sample_framebuffer[2], p.sample_framebuffer[3]]), + Uniform::new("capture_origin", [p.sample_framebuffer[0], p.sample_framebuffer[1]]), + Uniform::new("blur_radius", p.blur), + Uniform::new("presentation_alpha", p.presentation_alpha), + Uniform::new("background_opacity", p.background_opacity), + Uniform::new("tint", p.tint), ] } @@ -426,12 +426,12 @@ pub(super) fn material_uniforms(parameters: &MaterialParameters) -> Vec Vec> { vec![ - Uniform::new("paint_mode", paint_mode).into_owned(), - Uniform::new("shadow_rect", parameters.shadow_rect).into_owned(), - Uniform::new("shadow_values", parameters.shadow_values).into_owned(), - Uniform::new("visible_rect", parameters.visible_framebuffer).into_owned(), - Uniform::new("material_radius", parameters.radius).into_owned(), - Uniform::new("tint", parameters.tint).into_owned(), + Uniform::new("paint_mode", paint_mode), + Uniform::new("shadow_rect", parameters.shadow_rect), + Uniform::new("shadow_values", parameters.shadow_values), + Uniform::new("visible_rect", parameters.visible_framebuffer), + Uniform::new("material_radius", parameters.radius), + Uniform::new("tint", parameters.tint), ] } diff --git a/compositor/src/render/mod.rs b/compositor/src/render/mod.rs index 566e652..ed97737 100644 --- a/compositor/src/render/mod.rs +++ b/compositor/src/render/mod.rs @@ -4,6 +4,7 @@ mod materials; mod overview; mod resources; mod scene; +mod shader; use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -18,6 +19,7 @@ use scene::cursor_elements; pub(crate) use scene::{ animated_window_elements, frame_effect_metrics, layer_surfaces, output_elements, sampled_output_elements, }; +pub(crate) use shader::SharedPixelShaderElement; use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement; use smithay::backend::renderer::element::solid::SolidColorRenderElement; @@ -31,7 +33,6 @@ use smithay::backend::renderer::element::utils::{ use smithay::backend::renderer::element::{ Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage, render_elements, }; -use smithay::backend::renderer::gles::element::PixelShaderElement; use smithay::backend::renderer::gles::{ GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, UniformName, UniformType, @@ -66,7 +67,7 @@ render_elements! { Memory=MemoryRenderElement, Solid=SolidColorRenderElement, LockSurface=WaylandSurfaceRenderElement, - Border=PixelShaderElement, + Border=SharedPixelShaderElement, Blur=BlurRenderElement, Effect=PhysicalShaderElement, Native=NativeTextureElement, @@ -132,7 +133,7 @@ struct BlurRenderElement { commit: CommitCounter, geometry: Rectangle, capture_rect: [i32; 4], - uniforms: Vec>, + uniforms: Arc<[Uniform<'static>]>, } impl BlurRenderElement { @@ -150,7 +151,7 @@ impl BlurRenderElement { commit: CommitCounter::default(), geometry: parameters.sample_geometry, capture_rect: framebuffer_capture_rect(parameters.sample_framebuffer), - uniforms: blur_uniforms(parameters), + uniforms: blur_uniforms(parameters).into(), } } @@ -158,7 +159,7 @@ impl BlurRenderElement { self.capture_dirty.store(true, Ordering::Relaxed); self.geometry = parameters.sample_geometry; self.capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer); - self.uniforms = blur_uniforms(parameters); + self.uniforms = blur_uniforms(parameters).into(); self.commit.increment(); } } @@ -327,7 +328,7 @@ struct MaterialParameters { #[derive(Clone, Debug)] enum MaterialElement { - Fill(PixelShaderElement), + Fill(SharedPixelShaderElement), Blur(BlurRenderElement), } @@ -335,7 +336,7 @@ enum MaterialElement { struct CachedMaterial { element: MaterialElement, parameters: MaterialParameters, - shadow: PixelShaderElement, + shadow: SharedPixelShaderElement, } #[derive(Debug, Default)] @@ -361,7 +362,7 @@ struct BorderParameters { #[derive(Debug)] struct CachedBorder { - element: PixelShaderElement, + element: SharedPixelShaderElement, parameters: BorderParameters, } @@ -382,7 +383,7 @@ struct ShadowParameters { #[derive(Debug)] struct CachedShadow { - element: PixelShaderElement, + element: SharedPixelShaderElement, parameters: ShadowParameters, } diff --git a/compositor/src/render/overview.rs b/compositor/src/render/overview.rs index 354cb65..db5b005 100644 --- a/compositor/src/render/overview.rs +++ b/compositor/src/render/overview.rs @@ -40,12 +40,12 @@ pub(super) fn overview_chrome_element( border_uniforms(p) } else { vec![ - Uniform::new("visible_rect", p.clip_rect).into_owned(), - Uniform::new("material_radius", p.radius).into_owned(), - Uniform::new("tint", p.color).into_owned(), - Uniform::new("paint_mode", 0.0_f32).into_owned(), - Uniform::new("shadow_rect", p.clip_rect).into_owned(), - Uniform::new("shadow_values", [0.0_f32; 2]).into_owned(), + Uniform::new("visible_rect", p.clip_rect), + Uniform::new("material_radius", p.radius), + Uniform::new("tint", p.color), + Uniform::new("paint_mode", 0.0_f32), + Uniform::new("shadow_rect", p.clip_rect), + Uniform::new("shadow_values", [0.0_f32; 2]), ] } }; @@ -61,7 +61,7 @@ pub(super) fn overview_chrome_element( .or_default() .entry(context) .or_insert_with(|| CachedBorder { - element: PixelShaderElement::new( + element: SharedPixelShaderElement::new( program, geometry, None, @@ -119,33 +119,31 @@ pub(super) fn overview_strip_elements( .opacity; let mut elements = Vec::new(); let programs = rounded_clip_program(&mut state.render, renderer); - // Resolve IDs once per strip, rather than scanning all managed windows - // separately for every miniature. Own the handles so rendering can mutate state. - let windows_by_id: std::collections::HashMap<_, _> = state - .windows - .ids() + state.retain_overview_labels(); + + let card_ids = cards .iter() - .map(|(window, id)| (*id, window.clone())) - .collect(); + .map(|card| card.workspace.0) + .collect::>(); if let Some(output_id) = state.output_id(output) && let Some(cache) = state.render.outputs.get_mut(&output_id) { cache.chrome.retain(|(id, part), _| match part { - OverviewChromePart::Caption => windows_by_id.keys().any(|window| window.0 == *id), - OverviewChromePart::Card | OverviewChromePart::Outline => cards.iter().any(|card| card.workspace.0 == *id), + OverviewChromePart::Caption => state.windows.window(ferese_layout::WindowId(*id)).is_some(), + OverviewChromePart::Card | OverviewChromePart::Outline => card_ids.contains(id), OverviewChromePart::Strip => true, }); } // Cached title textures are independent of window content and stay readable // when thumbnails are small. Their pills use the shell's material and radius. - for (id, window) in &windows_by_id { + for id in frame.windows.keys() { if !state.window_belongs_to_output(*id, output) { continue; } let Some(rect) = frame.windows.get(id).map(|sample| sample.rect) else { continue; }; - let Some((buffer, size)) = state.overview_window_label(window, scale, rect.width) else { + let Some((buffer, size)) = state.overview_window_label(*id, scale, rect.width) else { continue; }; let width = (f64::from(size.w) / scale + 16.0).min(rect.width); @@ -222,7 +220,7 @@ pub(super) fn overview_strip_elements( } if let Some(programs) = &programs { for (id, rect) in &card.windows { - if let Some(window) = windows_by_id.get(id) { + if let Some(window) = state.windows.window(*id) { elements.extend(rounded_window_elements( renderer, window, diff --git a/compositor/src/render/scene.rs b/compositor/src/render/scene.rs index 6641f89..d792e55 100644 --- a/compositor/src/render/scene.rs +++ b/compositor/src/render/scene.rs @@ -1,3 +1,5 @@ +use smithay::desktop::Window; + use super::*; pub(crate) fn animated_window_elements( @@ -109,39 +111,42 @@ pub(crate) fn sampled_output_elements( frame, )); } - let mut candidates = if state.overview.is_active() { - state.windows.ids().keys().cloned().collect::>() + let prepare_window = |window: &Window| { + // Configure immediately, but present the frame/shadow only after + // the first buffer commit has settled the actual client geometry. + if !state.window_content_ready(window) { + return None; + } + let id = *state.windows.ids().get(window)?; + // Scrolling columns may sit outside their monitor's rectangle. + // They must not reappear on a neighboring output just because + // their global animated coordinates overlap it. + if !state.window_belongs_to_output(id, output) { + return None; + } + let sample = frame.windows.get(&id)?; + let visual = scaled_visual_rect(sample.rect, sample.close_scale); + let close_alpha = sample.close_alpha; + let decoration_progress = sample.geometry.decorations.clamp(0.0, 1.0); + + Some((window.clone(), id, sample, visual, decoration_progress, close_alpha)) + }; + let windows = if state.overview.is_active() { + state + .windows + .overview_windows() + .filter_map(prepare_window) + .collect::>() } else { - state.space.elements().rev().cloned().collect::>() + state + .space + .elements() + .rev() + .filter_map(prepare_window) + .collect::>() }; - if state.overview.is_active() { - candidates.sort_by_key(|window| std::cmp::Reverse(state.windows.ids().get(window).map_or(0, |id| id.0))); - } - let windows = candidates - .iter() - .filter_map(|window| { - // Configure immediately, but present the frame/shadow only after - // the first buffer commit has settled the actual client geometry. - if !state.window_content_ready(window) { - return None; - } - let id = *state.windows.ids().get(window)?; - // Scrolling columns may sit outside their monitor's rectangle. - // They must not reappear on a neighboring output just because - // their global animated coordinates overlap it. - if !state.window_belongs_to_output(id, output) { - return None; - } - let sample = frame.windows.get(&id)?; - let visual = scaled_visual_rect(sample.rect, sample.close_scale); - let close_alpha = sample.close_alpha; - let decoration_progress = sample.geometry.decorations.clamp(0.0, 1.0); - Some((window.clone(), id, visual, decoration_progress, close_alpha)) - }) - .collect::>(); - - for (window, id, visual, decoration_progress, close_alpha) in windows { + for (window, id, sample, visual, decoration_progress, close_alpha) in windows { let constrain = rounded_visual_rect(visual, output_geometry.loc); let material_surface = window .toplevel() @@ -158,10 +163,10 @@ pub(crate) fn sampled_output_elements( let corners = RoundedRect::new(visual, output_geometry.loc, scale, window_radius); let pixels = corners.rect; // Only overview/close intentionally scale the complete application. - let scale_content = frame.overview.is_presenting() || frame.windows[&id].close_scale != 1.0; + let scale_content = frame.overview.is_presenting() || sample.close_scale != 1.0; let behavior = resize_content_behavior(scale_content); - let dim = frame.windows[&id].dim; + let dim = sample.dim; if let Some(overlay) = window_tint_element( &mut state.render, renderer, @@ -182,7 +187,7 @@ pub(crate) fn sampled_output_elements( let shadow_blur = state.theme_settings.shadow_blur; let shadow_opacity = state.theme_settings.shadow_opacity; let shadow_color = state.theme_settings.shadow_color.0; - let focus = frame.windows[&id].focus; + let focus = sample.focus; let border_width = state.theme_settings.border_width + (state.theme_settings.focus_ring_width - state.theme_settings.border_width) * focus; let border_color = state.theme_settings.border_color.0; @@ -245,10 +250,7 @@ pub(crate) fn sampled_output_elements( ))), alpha: crate::presentation::handoff_alpha(snapshot.elapsed) * close_alpha, program: Some(programs.texture.clone()), - uniforms: vec![ - Uniform::new("clip_rect", clip).into_owned(), - Uniform::new("radius", corners.radius).into_owned(), - ], + uniforms: vec![Uniform::new("clip_rect", clip), Uniform::new("radius", corners.radius)], } .into(), ); @@ -260,7 +262,7 @@ pub(crate) fn sampled_output_elements( corners, scale, close_alpha, - frame.windows[&id].geometry.presentation_changed, + sample.geometry.presentation_changed, output, programs.clone(), behavior, diff --git a/compositor/src/render/shader.rs b/compositor/src/render/shader.rs new file mode 100644 index 0000000..4417581 --- /dev/null +++ b/compositor/src/render/shader.rs @@ -0,0 +1,231 @@ +//! Static uniform names stay borrowed; cached frame elements share their state. +//! Copy on write preserves the ID and commit seen by an already sampled frame. +use std::sync::Arc; + +use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement}; +use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, Uniform}; +use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions}; +use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform}; + +#[derive(Clone, Debug)] +pub(crate) struct SharedPixelShaderElement { + shader: GlesPixelProgram, + state: Arc, +} + +#[derive(Clone, Debug)] +struct ShaderState { + id: Id, + commit: CommitCounter, + area: Rectangle, + opaque: Vec>, + alpha: f32, + uniforms: Vec>, + kind: Kind, +} + +impl SharedPixelShaderElement { + pub(crate) fn new( + shader: GlesPixelProgram, + area: Rectangle, + opaque: Option>>, + alpha: f32, + uniforms: Vec>, + kind: Kind, + ) -> Self { + Self { + shader, + state: Arc::new(ShaderState { + id: Id::new(), + commit: CommitCounter::default(), + area, + opaque: opaque.unwrap_or_default(), + alpha, + uniforms, + kind, + }), + } + } + + pub(crate) fn resize(&mut self, area: Rectangle, opaque: Option>>) { + let opaque = opaque.unwrap_or_default(); + + if self.state.area != area || self.state.opaque != opaque { + let state = Arc::make_mut(&mut self.state); + state.area = area; + state.opaque = opaque; + state.commit.increment(); + } + } + + pub(crate) fn update_uniforms(&mut self, uniforms: Vec>) { + let state = Arc::make_mut(&mut self.state); + state.uniforms = uniforms; + state.commit.increment(); + } +} + +impl Element for SharedPixelShaderElement { + fn id(&self) -> &Id { + &self.state.id + } + + fn current_commit(&self) -> CommitCounter { + self.state.commit + } + + fn src(&self) -> Rectangle { + Rectangle::from_size(self.state.area.size.to_f64().to_buffer(1.0, Transform::Normal)) + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.state.area.to_physical_precise_round(scale) + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + self.state + .opaque + .iter() + .map(|region| region.to_physical_precise_round(scale)) + .collect() + } + + fn alpha(&self) -> f32 { + self.state.alpha + } + + fn kind(&self) -> Kind { + self.state.kind + } +} + +impl RenderElement for SharedPixelShaderElement { + fn draw( + &self, + frame: &mut GlesFrame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + _opaque: &[Rectangle], + ) -> Result<(), GlesError> { + frame.render_pixel_shader_to( + &self.shader, + src, + dst, + self.state.area.size.to_buffer(1, Transform::Normal), + Some(damage), + self.state.alpha, + &self.state.uniforms, + ) + } +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + + use smithay::backend::allocator::Fourcc; + use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; + use smithay::backend::renderer::gles::element::PixelShaderElement; + use smithay::backend::renderer::gles::{GlesTexture, UniformName, UniformType}; + use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer}; + + use super::*; + + fn pixels(renderer: &mut GlesRenderer, element: &impl RenderElement) -> Vec { + let size = (32, 32).into(); + let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap(); + let rect = Rectangle::from_size((32, 32).into()); + let mut target = renderer.bind(&mut texture).unwrap(); + + { + let mut frame = renderer + .render(&mut target, (32, 32).into(), Transform::Normal) + .unwrap(); + frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap(); + element.draw(&mut frame, element.src(), rect, &[rect], &[]).unwrap(); + let _ = frame.finish().unwrap(); + } + + let mapping = renderer + .copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888) + .unwrap(); + renderer.map_texture(&mapping).unwrap().to_vec() + } + + #[test] + #[ignore = "requires an EGL rendering device"] + fn shared_shader_matches_smithay_and_keeps_old_frames_immutable() { + let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); + let display = unsafe { EGLDisplay::new(device).unwrap() }; + let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; + let program = renderer + .compile_custom_pixel_shader( + super::super::corner_shader(super::super::ROUNDED_SOLID_SHADER), + &[ + UniformName::new("clip_rect", UniformType::_4f), + UniformName::new("radius", UniformType::_1f), + UniformName::new("color", UniformType::_4f), + ], + ) + .unwrap(); + let area = Rectangle::new((2, 3).into(), (32, 32).into()); + let uniforms = |color| { + vec![ + Uniform::new("clip_rect", [0.0_f32, 0.0, 32.0, 32.0]), + Uniform::new("radius", 8.0_f32), + Uniform::new("color", color), + ] + }; + let red = [0.5_f32, 0.0, 0.0, 0.5]; + let green = [0.0_f32, 0.5, 0.0, 0.5]; + let opaque = Some(vec![Rectangle::from_size((20, 20).into())]); + let baseline = PixelShaderElement::new( + program.clone(), + area, + opaque.clone(), + 0.5, + uniforms(red), + Kind::Unspecified, + ); + let mut shared = SharedPixelShaderElement::new(program, area, opaque, 0.5, uniforms(red), Kind::Unspecified); + let old = shared.clone(); + let old_commit = old.current_commit(); + + assert!(Arc::ptr_eq(&shared.state, &old.state)); + assert!( + shared + .state + .uniforms + .iter() + .all(|uniform| matches!(uniform.name, Cow::Borrowed(_))) + ); + + for scale in [1.0, 1.25, 2.0] { + assert_eq!(shared.geometry(scale.into()), baseline.geometry(scale.into())); + assert_eq!( + &*shared.opaque_regions(scale.into()), + &*baseline.opaque_regions(scale.into()) + ); + } + + assert_eq!(pixels(&mut renderer, &shared), pixels(&mut renderer, &baseline)); + shared.resize(area, Some(vec![Rectangle::from_size((20, 20).into())])); + assert!(Arc::ptr_eq(&shared.state, &old.state)); + shared.update_uniforms(uniforms(green)); + assert!(!Arc::ptr_eq(&shared.state, &old.state)); + assert_eq!(shared.id(), old.id()); + assert_eq!(old.current_commit(), old_commit); + assert_ne!(shared.current_commit(), old_commit); + assert_eq!(pixels(&mut renderer, &old), pixels(&mut renderer, &baseline)); + assert_ne!(pixels(&mut renderer, &shared), pixels(&mut renderer, &baseline)); + + let retained = shared.clone(); + shared.resize(Rectangle::from_size((16, 16).into()), None); + assert_eq!(retained.geometry(1.0.into()), area.to_physical_precise_round(1.0)); + assert_eq!(shared.geometry(1.0.into()).size, (16, 16).into()); + assert!(shared.opaque_regions(1.0.into()).is_empty()); + assert_eq!(shared.alpha(), baseline.alpha()); + assert_eq!(shared.kind(), baseline.kind()); + } +} diff --git a/compositor/src/shell_control.rs b/compositor/src/shell_control.rs index 4a2b711..4b48dff 100644 --- a/compositor/src/shell_control.rs +++ b/compositor/src/shell_control.rs @@ -4,8 +4,6 @@ use ferese_protocols::shell::v1::server::ferese_shell_manager_v1::FereseShellMan use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1; use ferese_protocols::shell::v1::server::{ferese_shell_manager_v1, ferese_shell_v1}; use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource}; -use smithay::wayland::compositor::with_states; -use smithay::wayland::shell::xdg::XdgToplevelSurfaceData; use crate::Ferese; use crate::private_client::ClientCapabilities; @@ -270,10 +268,15 @@ impl Ferese { self.last_shell_snapshot = None; return; } - let snapshot = self.shell_snapshot(); - if self.last_shell_snapshot.as_ref() == Some(&snapshot) { + if self + .last_shell_snapshot + .as_ref() + .is_some_and(|snapshot| self.shell_snapshot_matches(snapshot)) + { return; } + + let snapshot = self.shell_snapshot(); for resource in &self.shell_resources { if let Ok(shell) = resource.upgrade() { self.shell_snapshot_serial = self.shell_snapshot_serial.wrapping_add(1); @@ -283,6 +286,65 @@ impl Ferese { self.last_shell_snapshot = Some(snapshot); } + fn shell_snapshot_matches(&self, snapshot: &ShellSnapshot) -> bool { + let mut outputs = self.output_workspaces.connected_outputs(); + + for previous in &snapshot.outputs { + if outputs.next() != Some(previous.id) + || self.output_names.get(&previous.id) != Some(&previous.name) + || self.output_workspaces.active_workspace(previous.id) != Some(previous.active_workspace) + || (self.output_workspaces.focused_output() == Some(previous.id)) != previous.focused + { + return false; + } + } + + if outputs.next().is_some() { + return false; + } + + let mut workspaces = self.output_workspaces.workspace_views_iter(&self.workspaces); + + for previous in &snapshot.workspaces { + let Some(view) = workspaces.next() else { + return false; + }; + + if previous.id != view.id + || previous.output != Some(view.output) + || previous.index != view.index + || previous.window_count != view.window_count as u32 + || previous.visible != view.visible + || previous.focused != view.focused + { + return false; + } + } + + if workspaces.next().is_some() { + return false; + } + + let mut windows = self.windows.ordered_ids().filter_map(|id| self.shell_window_data(id)); + + for previous in &snapshot.windows { + let Some((id, workspace, app_id, title, state)) = windows.next() else { + return false; + }; + + if previous.id != id + || previous.workspace != workspace + || previous.app_id != app_id + || previous.title != title + || previous.state != state + { + return false; + } + } + + windows.next().is_none() + } + fn shell_snapshot(&self) -> ShellSnapshot { ShellSnapshot { outputs: self.output_snapshots(), @@ -319,8 +381,7 @@ impl Ferese { fn workspace_snapshots(&self) -> Vec { self.output_workspaces - .workspace_views(&self.workspaces) - .into_iter() + .workspace_views_iter(&self.workspaces) .map(|view| WorkspaceSnapshot { id: view.id, output: Some(view.output), @@ -333,56 +394,46 @@ impl Ferese { .collect() } + fn shell_window_data(&self, id: WindowId) -> Option<(WindowId, u64, &str, &str, ferese_shell_v1::WindowState)> { + let workspace = self.workspaces.workspace_for_window(id)?; + let record = self.windows.record(id)?; + self.windows.window(id)?.toplevel()?; + let mut state = ferese_shell_v1::WindowState::empty(); + + if self.focused_window == Some(id) { + state |= ferese_shell_v1::WindowState::Focused; + } + + if self + .workspaces + .workspace(workspace) + .is_some_and(|workspace| workspace.fullscreen == Some(id)) + { + state |= ferese_shell_v1::WindowState::Fullscreen; + } + + if matches!( + self.workspaces.placement(id), + Some(ferese_core::WindowPlacement::Floating { .. }) + ) { + state |= ferese_shell_v1::WindowState::Floating; + } + + Some((id, workspace.0, &record.app_id, &record.title, state)) + } + fn managed_window_snapshots(&self) -> Vec { - let mut snapshots = self - .windows - .ids() - .iter() - .filter_map(|(window, id)| { - let workspace = self.workspaces.workspace_for_window(*id)?.0; - let toplevel = window.toplevel()?; - let (app_id, title) = with_states(toplevel.wl_surface(), |states| { - let attributes = states - .data_map - .get::() - .expect("xdg toplevel state exists") - .lock() - .expect("xdg toplevel state is not poisoned"); - - ( - attributes.app_id.clone().unwrap_or_default(), - attributes.title.clone().unwrap_or_default(), - ) - }); - let mut state = ferese_shell_v1::WindowState::empty(); - - if self.focused_window == Some(*id) { - state |= ferese_shell_v1::WindowState::Focused; - } - if self - .workspaces - .workspace_for_window(*id) - .and_then(|workspace| self.workspaces.workspace(workspace)) - .is_some_and(|workspace| workspace.fullscreen == Some(*id)) - { - state |= ferese_shell_v1::WindowState::Fullscreen; - } - if self.is_floating_window(window) { - state |= ferese_shell_v1::WindowState::Floating; - } - - Some(ManagedWindowSnapshot { - id: *id, - workspace, - app_id, - title, - state, - }) + self.windows + .ordered_ids() + .filter_map(|id| self.shell_window_data(id)) + .map(|(id, workspace, app_id, title, state)| ManagedWindowSnapshot { + id, + workspace, + app_id: app_id.to_owned(), + title: title.to_owned(), + state, }) - .collect::>(); - - snapshots.sort_by_key(|snapshot| snapshot.id.0); - snapshots + .collect() } fn handle_shell_window_request( @@ -530,6 +581,66 @@ fn send_request_failed(shell: &FereseShellV1, request: ferese_shell_v1::FailedRe #[cfg(test)] mod tests { + #[test] + #[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"] + fn snapshot_precheck_detects_membership_selection_and_output_changes() { + use smithay::output::{Output, PhysicalProperties, Subpixel}; + use smithay::reexports::calloop::EventLoop; + use smithay::reexports::wayland_server::Display; + + use super::*; + + let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap()); + assert!(runtime.starts_with(std::env::temp_dir())); + let mut event_loop = EventLoop::try_new().unwrap(); + let config = crate::config::Config::default().runtime_config().unwrap(); + let mut state = Ferese::new(&mut event_loop, Display::new().unwrap(), config).unwrap(); + let output = Output::new( + "snapshot-test".into(), + PhysicalProperties { + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "test".into(), + model: "test".into(), + }, + ); + output.change_current_state( + Some(smithay::output::Mode { + size: (800, 600).into(), + refresh: 60_000, + }), + Some(smithay::utils::Transform::Normal), + None, + Some((0, 0).into()), + ); + state.space.map_output(&output, (0, 0)); + state.register_output(&output, "snapshot-test".into()); + let original = state.shell_snapshot(); + + for _ in 0..100 { + assert!(state.shell_snapshot_matches(&original)); + } + + state + .workspaces + .insert_window(WindowId(99), ferese_layout::Axis::Horizontal, 0.5) + .unwrap(); + assert!(!state.shell_snapshot_matches(&original)); + let occupied = state.shell_snapshot(); + assert!(state.shell_snapshot_matches(&occupied)); + let output_id = state.output_ids[&output]; + let next = state.workspaces.create_workspace(); + state.output_workspaces.assign_workspace(output_id, next).unwrap(); + assert!(!state.shell_snapshot_matches(&occupied)); + let assigned = state.shell_snapshot(); + state.output_workspaces.switch_workspace(output_id, next).unwrap(); + assert!(!state.shell_snapshot_matches(&assigned)); + let selected = state.shell_snapshot(); + assert!(state.shell_snapshot_matches(&selected)); + state.unregister_output(&output); + assert!(!state.shell_snapshot_matches(&selected)); + } + #[test] fn logout_serial_cannot_authorize_a_different_session_query() { assert!(super::logout_confirmation_matches(Some(42), Some(9), 42, 9)); diff --git a/compositor/src/stacking.rs b/compositor/src/stacking.rs index 3931ef0..fca7eec 100644 --- a/compositor/src/stacking.rs +++ b/compositor/src/stacking.rs @@ -21,6 +21,7 @@ pub(crate) fn layer_priority(floating: bool, zooming: bool, fullscreen: bool, ab pub(crate) struct WindowStack { order: Vec, ranks: HashMap, + revision: u64, } impl WindowStack { @@ -28,12 +29,14 @@ impl WindowStack { if let std::collections::hash_map::Entry::Vacant(entry) = self.ranks.entry(id) { entry.insert(self.order.len()); self.order.push(id); + self.revision = self.revision.wrapping_add(1); } } pub fn remove(&mut self, id: WindowId) { if let Some(rank) = self.ranks.remove(&id) { self.order.remove(rank); + self.revision = self.revision.wrapping_add(1); for (rank, candidate) in self.order.iter().enumerate().skip(rank) { self.ranks.insert(*candidate, rank); } @@ -41,6 +44,10 @@ impl WindowStack { } pub fn raise(&mut self, id: WindowId) { + if self.order.last() == Some(&id) { + return; + } + self.remove(id); self.insert(id); } @@ -48,12 +55,113 @@ impl WindowStack { pub fn rank(&self, id: WindowId) -> usize { self.ranks.get(&id).copied().unwrap_or(usize::MAX) } + + pub fn revision(&self) -> u64 { + self.revision + } +} + +/// Desired order survives coordinate-only remaps, which Smithay also raises. +pub(crate) struct StackingCache { + order: Vec, + revision: Option, + dirty: bool, + remapped: bool, +} + +impl Default for StackingCache { + fn default() -> Self { + Self { + order: Vec::new(), + revision: None, + dirty: true, + remapped: true, + } + } +} + +impl StackingCache { + pub fn invalidate(&mut self) { + self.dirty = true; + } + + pub fn remapped(&mut self) { + self.remapped = true; + } + + pub fn needs_rebuild(&self, revision: u64, mapped: usize) -> bool { + self.dirty || self.revision != Some(revision) || self.order.len() != mapped + } + + pub fn needs_restore(&self) -> bool { + self.remapped + } + + pub fn replace(&mut self, order: Vec, revision: u64) { + self.order = order; + self.revision = Some(revision); + self.dirty = false; + self.remapped = true; + } + + pub fn order(&self) -> &[W] { + &self.order + } + + pub fn restored(&mut self) { + self.remapped = false; + } } #[cfg(test)] mod tests { use super::*; + #[test] + fn clean_stacking_and_coordinate_remaps_do_not_require_resorting() { + let mut cache = StackingCache::default(); + assert!(cache.needs_rebuild(1, 2)); + cache.replace(vec!["first", "second"], 1); + cache.restored(); + let storage = cache.order().as_ptr(); + + for _ in 0..100 { + assert!(!cache.needs_rebuild(1, 2)); + assert!(!cache.needs_restore()); + assert_eq!(cache.order().as_ptr(), storage); + } + + cache.remapped(); + assert!(!cache.needs_rebuild(1, 2)); + assert!(cache.needs_restore()); + assert_eq!(cache.order(), ["first", "second"]); + cache.restored(); + assert!(cache.needs_rebuild(2, 2)); + assert!(cache.needs_rebuild(1, 1)); + cache.invalidate(); + assert!(cache.needs_rebuild(1, 2)); + } + + #[test] + fn stack_revision_changes_only_when_membership_or_rank_changes() { + let mut stack = WindowStack::default(); + stack.insert(WindowId(1)); + let first = stack.revision(); + stack.insert(WindowId(1)); + stack.raise(WindowId(1)); + stack.remove(WindowId(9)); + assert_eq!(stack.revision(), first); + + stack.insert(WindowId(2)); + let second = stack.revision(); + assert_ne!(second, first); + stack.raise(WindowId(1)); + assert_ne!(stack.revision(), second); + let raised = stack.revision(); + stack.remove(WindowId(1)); + assert_ne!(stack.revision(), raised); + } + #[test] fn cached_ranks_match_reference_order_after_mixed_operations() { let mut stack = WindowStack::default(); diff --git a/compositor/src/state.rs b/compositor/src/state.rs index a73da33..0ede6a0 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -76,7 +76,7 @@ use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconManager; use window_registry::WindowRegistry; use crate::backends::direct::DirectBackendState; -use crate::config::{Binding, DaemonConfig, InputSettings, OutputProfile, ThemeSettings}; +use crate::config::{Binding, BindingSet, DaemonConfig, InputSettings, OutputProfile, ThemeSettings}; use crate::cursor::{NamedCursor, cursor_theme, load_named_cursor}; use crate::dimming::DimAnimation; use crate::gestures::{Swipe, SwipeDirection}; @@ -91,7 +91,7 @@ use crate::portal_session::PortalSession; use crate::portal_shortcuts::PortalShortcuts; use crate::session_lock::{IdleSettings, Lock}; use crate::shell_control::ShellSnapshot; -use crate::stacking::WindowStack; +use crate::stacking::{StackingCache, WindowStack}; use crate::wallpaper::{WallpaperConfig, WallpaperState}; use crate::window_rules::{WindowRule, resolve as resolve_window_rules}; use crate::winit::NestedBackend; @@ -228,8 +228,13 @@ impl WorkspaceSlide { } fn progress(&self) -> f64 { + self.progress_at(Duration::ZERO) + } + + fn progress_at(&self, delta: Duration) -> f64 { self.held_progress.unwrap_or_else(|| { - smoothstep((self.elapsed.as_secs_f64() / WORKSPACE_SLIDE_DURATION.as_secs_f64()).min(1.0)) + let elapsed = self.elapsed.saturating_add(delta).min(WORKSPACE_SLIDE_DURATION); + smoothstep(elapsed.as_secs_f64() / WORKSPACE_SLIDE_DURATION.as_secs_f64()) }) } @@ -260,6 +265,7 @@ impl WorkspaceSlide { self.items.iter().any(|item| item.workspace == workspace) } + #[cfg(test)] fn offset(&self, workspace: WorkspaceId, width: f64, height: f64) -> (f64, f64) { let progress = self.progress(); let position = self @@ -323,12 +329,15 @@ pub struct Ferese { pub workspaces: WorkspaceSet, pub output_workspaces: OutputWorkspaceMap, pub(crate) output_ids: HashMap, + outputs_by_id: HashMap, + pub(crate) output_names: HashMap, output_identity_ids: HashMap, pub(crate) windows: WindowRegistry, pub(crate) render: crate::render::RenderResources, pub(crate) nested_backend: Option, pub(crate) wallpaper: WallpaperState, window_stack: WindowStack, + stacking_cache: StackingCache, floating_above_fullscreen: HashMap, floating_memory: crate::floating::Memory, floating_save_worker: crate::floating::SaveWorker, @@ -341,13 +350,14 @@ pub struct Ferese { viewport_animations: HashMap, focus_swipe: Option, workspace_slides: HashMap, + workspace_slide_offsets: HashMap, pub focused_window: Option, pub(crate) focus_history: ferese_core::FocusHistory, pub(crate) focus_cycle: Option, column_width_presets: Vec, gap_config: GapConfig, pub(crate) input_settings: InputSettings, - pub(crate) bindings: Vec, + pub(crate) bindings: BindingSet, workspace_auto_back_and_forth: bool, window_rules: Vec, pub(crate) theme_settings: ThemeSettings, @@ -543,12 +553,15 @@ impl Ferese { ), output_workspaces: OutputWorkspaceMap::default(), output_ids: HashMap::new(), + outputs_by_id: HashMap::new(), + output_names: HashMap::new(), output_identity_ids: HashMap::new(), windows: WindowRegistry::default(), render: Default::default(), nested_backend: None, wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())), window_stack: WindowStack::default(), + stacking_cache: StackingCache::default(), floating_above_fullscreen: HashMap::new(), floating_memory: crate::floating::Memory::path() .map(|p| crate::floating::Memory::load(&p)) @@ -563,13 +576,14 @@ impl Ferese { viewport_animations: HashMap::new(), focus_swipe: None, workspace_slides: HashMap::new(), + workspace_slide_offsets: HashMap::new(), focused_window: None, focus_history: Default::default(), focus_cycle: None, column_width_presets: config.column_width_presets, gap_config: config.gap_config, input_settings: config.input_settings, - bindings: config.bindings, + bindings: config.bindings.into(), workspace_auto_back_and_forth: config.workspace_auto_back_and_forth, window_rules: config.window_rules, theme_settings: config.theme_settings, @@ -707,7 +721,7 @@ impl Ferese { .map_err(|error| error.to_string())?; self.gap_config = config.gap_config; self.input_settings = config.input_settings; - self.bindings = config.bindings; + self.bindings = config.bindings.into(); self.workspace_auto_back_and_forth = config.workspace_auto_back_and_forth; self.portal_shortcuts.reconcile(&self.bindings, &self.input_settings); let old_rules = std::mem::replace(&mut self.window_rules, config.window_rules); @@ -717,6 +731,7 @@ impl Ferese { self.animations_enabled = config.animations_enabled; if !self.animations_enabled { self.workspace_slides.clear(); + self.workspace_slide_offsets.clear(); } self.animation_speed = config.animation_speed; self.spring_config = config.spring_config; @@ -976,6 +991,22 @@ impl ClientData for ClientState { #[cfg(test)] mod tests { + #[test] + fn sampling_slide_progress_matches_advancing_without_mutating_the_slide() { + let slide = WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Left); + + for delta in [Duration::ZERO, Duration::from_millis(8), Duration::from_secs(1)] { + let mut advanced = slide.clone(); + advanced.advance(delta); + assert_eq!(slide.progress_at(delta), advanced.progress()); + assert_eq!(slide.elapsed, WORKSPACE_SLIDE_FIRST_FRAME); + } + + let mut held = slide; + held.held_progress = Some(0.37); + assert_eq!(held.progress_at(Duration::from_secs(1)), 0.37); + } + #[test] fn configured_motion_policy_scales_durations_and_reduced_motion_wins() { for (source, milliseconds) in [ diff --git a/compositor/src/state/animation.rs b/compositor/src/state/animation.rs index 7dc2dd5..a11af6b 100644 --- a/compositor/src/state/animation.rs +++ b/compositor/src/state/animation.rs @@ -20,7 +20,56 @@ impl Ferese { self.last_animation_tick = Instant::now(); } + fn animations_need_tick(&self) -> bool { + if self.focus_swipe.is_some() + || !self.workspace_slides.is_empty() + || !self.dismissing_popups.is_empty() + || self.render.snapshots().next().is_some() + || self.overview.needs_tick() + { + return true; + } + + let moving = |value: &AnimatedValue| value.current != value.target || value.velocity != 0.0; + + if self.viewport_animations.iter().any(|(workspace, viewport)| { + self.output_workspaces + .output_for_workspace(*workspace) + .is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(*workspace)) + && moving(viewport) + }) { + return true; + } + + let selected = if self.overview.is_active() { + self.overview.selected() + } else { + self.focused_window + }; + + self.windows.records().any(|(&id, record)| { + let focus = if selected == Some(id) { 1.0 } else { 0.0 }; + let dim = crate::dimming::target( + self.inactive_dim, + self.focused_window, + id, + self.overview.is_presenting(), + ); + + record_needs_tick(record, focus, dim, || { + self.windows + .window(id) + .is_some_and(|window| self.space.element_location(window).is_some()) + }) + }) + } + pub(super) fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool { + if !self.animations_need_tick() { + self.sync_window_stacking(); + return false; + } + let delta = delta.mul_f64(self.animation_speed); if self .focus_swipe @@ -30,21 +79,6 @@ impl Ferese { self.focus_swipe = None; } - let visible_workspaces = if self.viewport_animations.is_empty() { - HashSet::new() - } else { - self.space - .outputs() - .filter_map(|output| self.output_ids.get(output)) - .filter_map(|output| self.output_workspaces.active_workspace(*output)) - .collect::>() - }; - - let windows = self - .space - .elements() - .filter_map(|window| self.windows.ids().get(window).copied().map(|id| (window.clone(), id))) - .collect::>(); let mut active_animation = false; let mut completed_slides = Vec::new(); for (output, slide) in &mut self.workspace_slides { @@ -102,9 +136,12 @@ impl Ferese { dim_changed |= previous != focus.current; } - for (_, id) in &windows { - let target = crate::dimming::target(dim_settings, self.focused_window, *id, self.overview.is_presenting()); - let Some(record) = self.windows.record_mut(*id) else { + for window in self.space.elements() { + let Some(id) = self.windows.ids().get(window).copied() else { + continue; + }; + let target = crate::dimming::target(dim_settings, self.focused_window, id, self.overview.is_presenting()); + let Some(record) = self.windows.record_mut(id) else { continue; }; @@ -184,7 +221,11 @@ impl Ferese { // Keep scheduling frames while waiting, so the deadline cannot stall. active_animation |= !blocked_workspaces.is_empty(); for (workspace, viewport) in &mut self.viewport_animations { - if !visible_workspaces.contains(workspace) { + if !self + .output_workspaces + .output_for_workspace(*workspace) + .is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(*workspace)) + { continue; } @@ -202,8 +243,13 @@ impl Ferese { } } - let mut settled_coupled_widths = Vec::new(); - for (window, id) in windows { + let mut remaps = Vec::new(); + + for window in self.space.elements() { + let Some(id) = self.windows.ids().get(window).copied() else { + continue; + }; + if self .workspaces .workspace_for_window(id) @@ -229,6 +275,11 @@ impl Ferese { geometry.visual.velocity.width = 0.0; } active_animation |= geometry.advance(delta, self.spring_config, self.animations_enabled); + + if zooming != geometry.is_zooming() { + self.stacking_cache.invalidate(); + } + if let Some(target) = coupled_target { geometry.visual.target.width = target; } @@ -262,7 +313,7 @@ impl Ferese { geometry.visual.current.width = width.current; geometry.visual.velocity.width = width.velocity; if !width_active { - settled_coupled_widths.push(id); + record.coupled_width = None; } } @@ -287,12 +338,18 @@ impl Ferese { } let visual = geometry.visual.current; - self.space - .map_element(window, (visual.x.round() as i32, visual.y.round() as i32), false); + let location = (visual.x.round() as i32, visual.y.round() as i32).into(); + + if self.space.element_location(window) != Some(location) { + remaps.push((id, location)); + } } - for id in settled_coupled_widths { - self.windows.update(id, |record| record.coupled_width = None); + for (id, location) in remaps { + if let Some(window) = self.windows.window(id) { + self.space.map_element(window.clone(), location, false); + self.stacking_cache.remapped(); + } } if !completed_slides.is_empty() { @@ -320,6 +377,7 @@ impl Ferese { active_animation |= self .overview .advance(delta, self.spring_config, self.animations_enabled); + self.refresh_workspace_slide_offsets(); self.sync_window_stacking(); if active_animation || dim_changed { @@ -337,48 +395,173 @@ impl Ferese { } pub(crate) fn workspace_slide_offset(&self, window: WindowId) -> (f64, f64) { - self.workspace_slide_offset_at(window, Duration::ZERO) + self.workspaces + .workspace_for_window(window) + .and_then(|workspace| self.workspace_slide_offsets.get(&workspace).copied()) + .unwrap_or_default() } - pub(super) fn workspace_slide_offset_at(&self, window: WindowId, delta: Duration) -> (f64, f64) { - let Some(workspace) = self.workspaces.workspace_for_window(window) else { - return (0.0, 0.0); - }; + pub(super) fn sample_workspace_slide_offsets( + &self, + output: Option, + delta: Duration, + ) -> HashMap { + let mut offsets = HashMap::new(); + self.fill_workspace_slide_offsets(output, delta, &mut offsets); + offsets + } - let Some(output_id) = self - .workspace_slides - .iter() - .find_map(|(output, slide)| slide.contains(workspace).then_some(*output)) - .or_else(|| self.output_workspaces.output_for_workspace(workspace)) - else { - return (0.0, 0.0); - }; + pub(super) fn refresh_workspace_slide_offsets(&mut self) { + // Reuse the input snapshot's allocation. Predicted render snapshots + // remain separate and never change hit-testing or gesture state. + let mut offsets = std::mem::take(&mut self.workspace_slide_offsets); + offsets.clear(); + self.fill_workspace_slide_offsets(None, Duration::ZERO, &mut offsets); + self.workspace_slide_offsets = offsets; + } - let Some(slide) = self.workspace_slides.get(&output_id) else { - return (0.0, 0.0); - }; + fn fill_workspace_slide_offsets( + &self, + output: Option, + delta: Duration, + offsets: &mut HashMap, + ) { + for (output_id, slide) in &self.workspace_slides { + if output.is_some_and(|output| output != *output_id) { + continue; + } - let Some(size) = self - .output_ids - .iter() - .find(|(_, id)| **id == output_id) - .and_then(|(output, _)| self.space.output_geometry(output)) - .map(|geometry| geometry.size) - else { - return (0.0, 0.0); - }; + let Some(size) = self + .outputs_by_id + .get(output_id) + .and_then(|output| self.space.output_geometry(output)) + .map(|geometry| geometry.size) + else { + continue; + }; + let progress = slide.progress_at(delta); - let mut slide = slide.clone(); - if slide.held_progress.is_none() && !delta.is_zero() { - slide.advance(delta); + for item in &slide.items { + let position = item.start.between(item.target, progress); + offsets.insert( + item.workspace, + (position.x * f64::from(size.w), position.y * f64::from(size.h)), + ); + } } - - slide.offset(workspace, f64::from(size.w), f64::from(size.h)) } pub(crate) fn cancel_workspace_slides(&mut self) -> bool { let active = !self.workspace_slides.is_empty(); self.workspace_slides.clear(); + self.workspace_slide_offsets.clear(); active } } + +// Inspect current targets, not the previous frame's activity. Target changes +// and client waits must wake a desktop that was settled on its last tick. +fn record_needs_tick( + record: &super::window_registry::WindowRecord, + focus: f64, + dim: f64, + mapped: impl FnOnce() -> bool, +) -> bool { + if record.resize.is_some() + || record.closing.as_ref().is_some_and(|close| !close.close_sent) + || record.focus.as_ref().is_none_or(|motion| motion.needs_update(focus)) + { + return true; + } + + let moving = |value: &AnimatedValue| value.current != value.target || value.velocity != 0.0; + let pending = record.dimming.as_ref().is_none_or(|motion| motion.needs_update(dim)) + || record.world_x.as_ref().is_some_and(|(_, world)| moving(world)) + || record.coupled_width.is_some() + || record.geometry.is_some_and(|geometry| { + geometry.is_zooming() + || geometry.presentation_changed + || geometry.client.waiting_for_commit() + || geometry.visual.current != geometry.visual.target + || geometry.visual.velocity != ferese_animation::RectVelocity::default() + || (!record.natural_floating_pending + && geometry.client.last_configured_size != Some(ClientSize::from_rect(geometry.logical))) + }); + + // Hidden geometry and dimming do not advance in the full path. + // A stale hidden target must not keep waking the whole desktop. + pending && mapped() +} + +#[cfg(test)] +mod tests { + use super::super::window_registry::WindowRecord; + use super::*; + + fn settled() -> WindowRecord { + let rect = Rect::new(0.0, 0.0, 400.0, 300.0); + let mut geometry = WindowGeometry::new(rect, Some(ClientSize::from_rect(rect))); + geometry.presentation_changed = false; + + WindowRecord { + geometry: Some(geometry), + focus: Some(DimAnimation::new(1.0)), + dimming: Some(DimAnimation::new(0.0)), + ..Default::default() + } + } + + #[test] + fn idle_check_skips_mapping_lookup_but_new_focus_and_dim_targets_wake_it() { + let record = settled(); + + for _ in 0..100 { + assert!(!record_needs_tick(&record, 1.0, 0.0, || panic!( + "settled record needs no mapping lookup" + ))); + } + + assert!(record_needs_tick(&record, 0.0, 0.0, || true)); + assert!(record_needs_tick(&record, 1.0, 0.15, || true)); + assert!(!record_needs_tick(&record, 1.0, 0.15, || false)); + } + + #[test] + fn geometry_motion_client_waits_and_resize_barriers_cannot_be_skipped() { + let mut record = settled(); + record + .geometry + .as_mut() + .unwrap() + .visual + .set_target(Rect::new(10.0, 0.0, 400.0, 300.0)); + assert!(record_needs_tick(&record, 1.0, 0.0, || true)); + assert!(!record_needs_tick(&record, 1.0, 0.0, || false)); + + let mut record = settled(); + record.geometry.as_mut().unwrap().client.request_size( + ClientSize { + width: 500, + height: 300, + }, + Duration::ZERO, + ); + assert!(record_needs_tick(&record, 1.0, 0.0, || true)); + record.resize = Some(crate::resize_transaction::ResizeTransaction::new( + 1.into(), + Duration::ZERO, + )); + assert!(record_needs_tick(&record, 1.0, 0.0, || false)); + } + + #[test] + fn close_requests_and_coupled_width_cleanup_still_get_their_final_tick() { + let mut record = settled(); + record.closing = Some(ClosingAnimation::default()); + assert!(record_needs_tick(&record, 1.0, 0.0, || false)); + record.closing.as_mut().unwrap().close_sent = true; + assert!(!record_needs_tick(&record, 1.0, 0.0, || true)); + record.coupled_width = Some((WorkspaceId(1), AnimatedValue::new(400.0))); + assert!(record_needs_tick(&record, 1.0, 0.0, || true)); + } +} diff --git a/compositor/src/state/floating.rs b/compositor/src/state/floating.rs index 741c970..e3ea2a4 100644 --- a/compositor/src/state/floating.rs +++ b/compositor/src/state/floating.rs @@ -17,12 +17,7 @@ impl Ferese { Some(WindowPlacement::Floating { rect }) => rect, None => return, }; - if let Some(handle) = self - .windows - .ids() - .iter() - .find_map(|(handle, id)| (*id == window).then_some(handle.clone())) - { + if let Some(handle) = self.windows.window(window).cloned() { if matches!( self.workspaces.placement(window), Some(WindowPlacement::Floating { .. }) @@ -178,6 +173,10 @@ impl Ferese { .geometry .get_or_insert_with(|| WindowGeometry::new(rect, client_size(window))); + if geometry.is_zooming() { + self.stacking_cache.invalidate(); + } + if let Some(size) = geometry.follow_pointer(rect, self.start_time.elapsed()) && let Some(toplevel) = window.toplevel() { @@ -188,7 +187,7 @@ impl Ferese { } self.windows.update(id, |record| record.world_x = None); self.windows.update(id, |record| record.coupled_width = None); - self.space.map_element(window.clone(), location, false); + self.map_window_geometry(window.clone(), location); self.sync_window_stacking(); // Move/resize/cancel callbacks run with Smithay's pointer mutex held. // Cursor rendering reads current_location(), which would lock it again. @@ -269,14 +268,7 @@ impl Ferese { let (app, parent) = Self::floating_metadata(window); let parent_rect = parent .as_ref() - .and_then(|surface| { - self.windows.ids().iter().find_map(|(window, id)| { - window - .toplevel() - .is_some_and(|top| top.wl_surface() == surface) - .then_some(*id) - }) - }) + .and_then(|surface| self.windows.id_for_surface(surface)) .and_then(|parent| self.presented_window_rect(parent)) .and_then(|rect| intersection(rect, work)); let dialog = crate::window_rules::is_native_dialog(app.as_deref()); @@ -420,10 +412,9 @@ impl Ferese { .workspaces .workspace_for_window(window) .and_then(|workspace| self.output_workspaces.output_for_workspace(workspace)); - self.output_ids - .iter() - .find(|(_, id)| Some(**id) == output_id) - .and_then(|(output, _)| self.output_bounds_for(output)) + output_id + .and_then(|id| self.outputs_by_id.get(&id)) + .and_then(|output| self.output_bounds_for(output)) .or_else(|| self.output_bounds()) } diff --git a/compositor/src/state/focus.rs b/compositor/src/state/focus.rs index d0397b3..3afc4f7 100644 --- a/compositor/src/state/focus.rs +++ b/compositor/src/state/focus.rs @@ -49,7 +49,7 @@ impl Ferese { let available = self.focus_candidates(); if !preview { self.cancel_focus_cycle(); - let order = self.focus_history.candidates(self.focused_window, available.clone()); + let order = self.focus_history.candidates(self.focused_window, &available); let mut cycle = ferese_core::FocusCycle::new(order, self.focused_window); if let Some(id) = cycle.advance(reverse, &available) { self.activate_managed_window(id); @@ -57,7 +57,7 @@ impl Ferese { return; } if self.focus_cycle.is_none() { - let order = self.focus_history.candidates(self.focused_window, available.clone()); + let order = self.focus_history.candidates(self.focused_window, &available); if order.len() < 2 { return; } diff --git a/compositor/src/state/hit_testing.rs b/compositor/src/state/hit_testing.rs index 4ce8e43..3b3e881 100644 --- a/compositor/src/state/hit_testing.rs +++ b/compositor/src/state/hit_testing.rs @@ -166,10 +166,7 @@ impl Ferese { }; if overview_active { - let mut candidates = self.windows.ids().keys().cloned().collect::>(); - // Match Overview's front-to-back render order during overlapping motion. - candidates.sort_by_key(|window| std::cmp::Reverse(self.windows.ids().get(window).map_or(0, |id| id.0))); - candidates.iter().find_map(hit) + self.windows.overview_windows().find_map(hit) } else { self.space.elements().rev().find_map(hit) } diff --git a/compositor/src/state/layout.rs b/compositor/src/state/layout.rs index 51e9896..c6a2d49 100644 --- a/compositor/src/state/layout.rs +++ b/compositor/src/state/layout.rs @@ -59,6 +59,7 @@ impl Ferese { // Consume idle time before setting new targets; it must not become a // large first animation step after a keypress on an idle desktop. self.advance_animations(Instant::now()); + self.stacking_cache.invalidate(); self.arrange_layers(); let mut previous_scrolling_world_x = self.windows.take_world_positions(); let pending_column_width_cycles = self.windows.take_column_width_requests(); @@ -303,7 +304,7 @@ impl Ferese { let location = (visual.x.round() as i32, visual.y.round() as i32); - self.space.map_element(window.clone(), location, false); + self.map_window_geometry(window.clone(), location.into()); if let Some(toplevel) = window.toplevel() { let state_changed = toplevel.with_pending_state(|state| { if natural_pending { @@ -372,39 +373,66 @@ impl Ferese { } self.space.raise_element(window, activate); + self.stacking_cache.remapped(); self.sync_window_stacking(); } + pub(crate) fn invalidate_window_stacking(&mut self) { + self.stacking_cache.invalidate(); + } + pub(crate) fn sync_window_stacking(&mut self) { - let mut windows = self.space.elements().cloned().collect::>(); - windows.sort_by_key(|window| { - let id = self.windows.ids().get(window).copied(); - let geometry = id.and_then(|id| self.windows.geometry(&id)); - let floating = - id.is_some_and(|id| matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }))); - let priority = crate::stacking::layer_priority( - floating, - geometry.is_some_and(|geometry| geometry.is_zooming()), - geometry.is_some_and(|geometry| geometry.is_fullscreen()), - floating - && id.is_some_and(|id| { - self.floating_above_fullscreen.get(&id).is_some_and(|parent| { - self.workspaces - .workspace_for_window(id) - .and_then(|workspace| self.workspaces.workspace(workspace)) - .is_some_and(|workspace| workspace.fullscreen == Some(*parent)) - }) - }), - ); - (priority, id.map_or(usize::MAX, |id| self.window_stack.rank(id))) - }); - if stacking_order_settled(self.space.elements(), &windows, |window| window.z_index()) { + let revision = self.window_stack.revision(); + let rebuild = self.stacking_cache.needs_rebuild(revision, self.space.elements().len()); + + if !rebuild && !self.stacking_cache.needs_restore() { return; } - for window in windows { - self.space.raise_element(&window, false); + if rebuild { + let mut windows = self.space.elements().cloned().collect::>(); + windows.sort_by_key(|window| { + let id = self.windows.ids().get(window).copied(); + let geometry = id.and_then(|id| self.windows.geometry(&id)); + let floating = id + .is_some_and(|id| matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }))); + let priority = crate::stacking::layer_priority( + floating, + geometry.is_some_and(|geometry| geometry.is_zooming()), + geometry.is_some_and(|geometry| geometry.is_fullscreen()), + floating + && id.is_some_and(|id| { + self.floating_above_fullscreen.get(&id).is_some_and(|parent| { + self.workspaces + .workspace_for_window(id) + .and_then(|workspace| self.workspaces.workspace(workspace)) + .is_some_and(|workspace| workspace.fullscreen == Some(*parent)) + }) + }), + ); + (priority, id.map_or(usize::MAX, |id| self.window_stack.rank(id))) + }); + self.stacking_cache.replace(windows, revision); } + + if !stacking_order_settled(self.space.elements(), self.stacking_cache.order(), |window| { + window.z_index() + }) { + for window in self.stacking_cache.order() { + self.space.raise_element(window, false); + } + } + + self.stacking_cache.restored(); + } + + pub(crate) fn map_window_geometry(&mut self, window: Window, location: Point) { + if self.space.element_location(&window) == Some(location) { + return; + } + + self.space.map_element(window, location, false); + self.stacking_cache.remapped(); } pub(super) fn tiled_layout(&mut self, bounds: Rect) -> Result { diff --git a/compositor/src/state/navigation.rs b/compositor/src/state/navigation.rs index b34e634..07b4b2f 100644 --- a/compositor/src/state/navigation.rs +++ b/compositor/src/state/navigation.rs @@ -139,12 +139,7 @@ impl Ferese { { tracing::warn!(%error, ?current, ?next, "failed to slide swipe focus"); } - let Some(window) = self - .windows - .ids() - .iter() - .find_map(|(window, id)| (*id == next).then(|| window.clone())) - else { + let Some(window) = self.windows.window(next).cloned() else { return; }; let Some(surface) = window.toplevel().map(|toplevel| toplevel.wl_surface().clone()) else { @@ -306,12 +301,7 @@ impl Ferese { } pub(crate) fn activate_managed_window(&mut self, id: WindowId) -> bool { - let Some(window) = self - .windows - .ids() - .iter() - .find_map(|(window, window_id)| (*window_id == id).then(|| window.clone())) - else { + let Some(window) = self.windows.window(id).cloned() else { return false; }; if !activate_window_workspace(&mut self.workspaces, &mut self.output_workspaces, id) { @@ -441,6 +431,7 @@ impl Ferese { .get_mut(&output) .expect("gesture output exists") .held_progress = Some(progress); + self.refresh_workspace_slide_offsets(); self.backdrop_generation = self.backdrop_generation.wrapping_add(1); self.cursor_redraw_pending = true; return; @@ -555,14 +546,11 @@ impl Ferese { self.focus_lock_surface(); return; } - let surface = self.focused_window.and_then(|focused| { - self.windows.ids().iter().find_map(|(window, id)| { - (*id == focused) - .then(|| window.toplevel()) - .flatten() - .map(|toplevel| toplevel.wl_surface().clone()) - }) - }); + let surface = self + .focused_window + .and_then(|focused| self.windows.window(focused)) + .and_then(|window| window.toplevel()) + .map(|toplevel| toplevel.wl_surface().clone()); self.seat.get_keyboard().expect("seat has a keyboard").set_focus( self, diff --git a/compositor/src/state/outputs.rs b/compositor/src/state/outputs.rs index 649d786..b3dd551 100644 --- a/compositor/src/state/outputs.rs +++ b/compositor/src/state/outputs.rs @@ -29,6 +29,8 @@ impl Ferese { match registration { Ok(_) => { self.output_ids.insert(output.clone(), output_id); + self.outputs_by_id.insert(output_id, output.clone()); + self.output_names.insert(output_id, output.name()); self.restore_output_focus(); let visible = self.visible_workspace_ids(); self.reconcile_workspaces(&visible); @@ -45,6 +47,8 @@ impl Ferese { return; }; + self.outputs_by_id.remove(&output_id); + self.output_names.remove(&output_id); self.workspace_slides.remove(&output_id); self.render.remove_output(output_id); self.pending_screencopies.retain(|capture| { @@ -113,9 +117,7 @@ impl Ferese { pub(crate) fn focused_output(&self) -> Option<&Output> { let focused = self.output_workspaces.focused_output()?; - self.output_ids - .iter() - .find_map(|(output, id)| (*id == focused).then_some(output)) + self.outputs_by_id.get(&focused) } pub(crate) fn restore_output_focus(&mut self) { diff --git a/compositor/src/state/prediction.rs b/compositor/src/state/prediction.rs index 8de7f5b..3fc9000 100644 --- a/compositor/src/state/prediction.rs +++ b/compositor/src/state/prediction.rs @@ -5,6 +5,8 @@ use super::*; pub(crate) struct FrameScene { pub windows: HashMap, pub overview: crate::overview::OverviewMotion, + // Plane selection and scheduling reuse the activity sampled with this scene. + pub animating: bool, } pub(crate) struct WindowFrame { @@ -17,6 +19,31 @@ pub(crate) struct WindowFrame { } impl Ferese { + fn output_window_records<'a>( + &'a self, + output: &'a Output, + block_resizes: bool, + ) -> impl Iterator + 'a { + self.output_id(output) + .into_iter() + .flat_map(move |output| self.output_workspaces.assigned_workspaces(output)) + .filter_map(move |id| self.workspaces.workspace(id)) + .flat_map(move |workspace| { + let blocked = block_resizes + && workspace + .layout + .window_ids() + .chain(workspace.floating.iter().copied()) + .any(|id| self.windows.record(id).is_some_and(|record| record.resize.is_some())); + + workspace + .layout + .window_ids() + .chain(workspace.floating.iter().copied()) + .filter_map(move |id| Some((id, self.windows.record(id)?, blocked))) + }) + } + pub(crate) fn output_has_animations(&self, output: &Output) -> bool { if self.overview.is_animating(self.spring_config) || !self.dismissing_popups.is_empty() { return true; @@ -30,17 +57,17 @@ impl Ferese { return true; } - self.windows.records().any(|(id, record)| { + self.output_window_records(output, false).any(|(id, record, _)| { let Some(mut geometry) = record.geometry else { return false; }; - if !self.window_belongs_to_output(*id, output) { + if !self.window_belongs_to_output(id, output) { return false; } if record.resize.is_some() - || self.render.snapshot(id).is_some() + || self.render.snapshot(&id).is_some() || record.closing.as_ref().is_some_and(|close| !close.close_sent) || record.focus.as_ref().is_some_and(|focus| focus.is_animating()) || record.dimming.as_ref().is_some_and(|dim| dim.is_animating()) @@ -52,7 +79,7 @@ impl Ferese { return true; } - let Some(workspace) = self.workspaces.workspace_for_window(*id) else { + let Some(workspace) = self.workspaces.workspace_for_window(id) else { return false; }; @@ -71,28 +98,28 @@ impl Ferese { } pub(crate) fn sample_frame(&self, output: &Output, horizon: Duration) -> FrameScene { - let delta = if self.animations_enabled && self.output_has_animations(output) { + let animating = self.output_has_animations(output); + let delta = if self.animations_enabled && animating { horizon.min(Duration::from_millis(100)).mul_f64(self.animation_speed) } else { Duration::ZERO }; let overview = self.overview.sample(delta, self.spring_config); - let blocked = self - .windows - .resizing() - .filter_map(|id| self.workspaces.workspace_for_window(*id)) - .collect::>(); + let predicted_slide_offsets = + (!delta.is_zero()).then(|| self.sample_workspace_slide_offsets(self.output_id(output), delta)); + let slide_offsets = predicted_slide_offsets + .as_ref() + .unwrap_or(&self.workspace_slide_offsets); let mut windows = HashMap::new(); - for (&id, record) in self.windows.records() { + for (id, record, blocked) in self.output_window_records(output, !delta.is_zero()) { let Some(original) = record.geometry else { continue }; if !self.window_belongs_to_output(id, output) { continue; } - let workspace = self.workspaces.workspace_for_window(id); let world = record.world_x.as_ref().and_then(|(workspace, world)| { let viewport = self.viewport_animations.get(workspace)?; let held = self @@ -103,7 +130,7 @@ impl Ferese { Some((*world, *viewport, held)) }); let width = record.coupled_width.as_ref().map(|(_, width)| *width); - let geometry = if delta.is_zero() || workspace.is_some_and(|workspace| blocked.contains(&workspace)) { + let geometry = if delta.is_zero() || blocked { original } else { predict_geometry( @@ -118,7 +145,11 @@ impl Ferese { let mut rect = overview.presented_rect(id, geometry.visual.current); if !overview.is_presenting() { - let (x, y) = self.workspace_slide_offset_at(id, delta); + let (x, y) = self + .workspaces + .workspace_for_window(id) + .and_then(|workspace| slide_offsets.get(&workspace).copied()) + .unwrap_or_default(); rect.x += x; rect.y += y; } @@ -160,7 +191,11 @@ impl Ferese { ); } - FrameScene { windows, overview } + FrameScene { + windows, + overview, + animating, + } } } @@ -259,12 +294,109 @@ mod tests { { let forecast = state.sample_frame(&output, horizon); assert!(forecast.windows[&id].geometry.visual.current.x > geometry.visual.current.x); + assert!(forecast.animating); } assert_eq!(*state.windows.geometry(&id).unwrap(), geometry); assert_eq!(state.last_animation_tick, tick); } + // Sampling one output must not visit or include another output's records. + let other = Output::new( + "other-forecast".into(), + smithay::output::PhysicalProperties { + size: (0, 0).into(), + subpixel: smithay::output::Subpixel::Unknown, + make: "test".into(), + model: "test".into(), + }, + ); + other.change_current_state( + Some(smithay::output::Mode { + size: (1280, 720).into(), + refresh: 240_000, + }), + Some(smithay::utils::Transform::Normal), + None, + Some((1920, 0).into()), + ); + state.space.map_output(&other, (1920, 0)); + state.register_output(&other, "other-forecast".into()); + let other_workspace = state + .output_workspaces + .active_workspace(state.output_id(&other).unwrap()) + .unwrap(); + let other_id = WindowId(2); + state + .workspaces + .insert_floating_window(other_id, other_workspace, geometry.visual.current, false) + .unwrap(); + state.windows.records.insert( + other_id, + super::super::window_registry::WindowRecord { + geometry: Some(geometry), + ..Default::default() + }, + ); + + assert_eq!( + state + .output_window_records(&output, false) + .map(|(id, _, _)| id) + .collect::>(), + vec![id] + ); + assert_eq!( + state + .output_window_records(&other, false) + .map(|(id, _, _)| id) + .collect::>(), + vec![other_id] + ); + assert!( + !state + .sample_frame(&output, Duration::from_millis(8)) + .windows + .contains_key(&other_id) + ); + assert!( + !state + .sample_frame(&other, Duration::from_millis(4)) + .windows + .contains_key(&id) + ); + + let output_id = state.output_id(&output).unwrap(); + let from = state.workspaces.workspace_for_window(id).unwrap(); + let to = state.workspaces.create_workspace(); + state.output_workspaces.assign_workspace(output_id, to).unwrap(); + let mut slide = WorkspaceSlide::new(None, from, to, SwipeDirection::Left); + slide.held_progress = Some(0.25); + state.workspace_slides.insert(output_id, slide); + state.refresh_workspace_slide_offsets(); + let offsets = state.workspace_slide_offsets.clone(); + assert_eq!(offsets[&from].0, -480.0); + assert_eq!(offsets[&to].0, 1440.0); + assert_eq!( + state.sample_frame(&output, Duration::from_millis(8)).windows[&id] + .rect + .x, + state.sample_frame(&output, Duration::from_millis(8)).windows[&id] + .geometry + .visual + .current + .x + - 480.0 + ); + assert_eq!(state.workspace_slide_offsets, offsets); + assert!( + state + .sample_workspace_slide_offsets(state.output_id(&other), Duration::from_millis(8)) + .is_empty() + ); + state.cancel_workspace_slides(); + assert!(state.workspace_slide_offsets.is_empty()); + // A prediction must neither advance nor expire a client resize barrier. state.windows.set_transaction( id, @@ -278,6 +410,12 @@ mod tests { state.animations_enabled = false; let forecast = state.sample_frame(&output, Duration::from_millis(16)); assert_eq!(forecast.windows[&id].geometry, geometry); + + let settled = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None); + state.windows.set_geometry(id, settled); + state.animations_enabled = true; + assert!(!state.sample_frame(&output, Duration::ZERO).animating); + assert!(state.sample_frame(&other, Duration::ZERO).animating); } #[test] diff --git a/compositor/src/state/window_registry.rs b/compositor/src/state/window_registry.rs index 21afc47..eb43f4c 100644 --- a/compositor/src/state/window_registry.rs +++ b/compositor/src/state/window_registry.rs @@ -4,6 +4,10 @@ use std::hash::Hash; use ferese_animation::{AnimatedValue, WindowGeometry}; use ferese_core::WorkspaceId; use ferese_layout::{ColumnWidth, Rect, WindowId}; +use smithay::desktop::Window; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::wayland::compositor::with_states; +use smithay::wayland::shell::xdg::XdgToplevelSurfaceData; use crate::resize_transaction::ResizeTransaction; @@ -12,6 +16,10 @@ use crate::resize_transaction::ResizeTransaction; #[derive(Default)] pub(crate) struct WindowRecord { pub geometry: Option, + pub surface: Option, + pub app_id: String, + pub title: String, + pub metadata_revision: u64, pub resize: Option, pub focus: Option, pub dimming: Option, @@ -32,7 +40,11 @@ pub(crate) struct WindowRecord { /// handle always updates its record as well. IDs are never reused on remapping. pub(crate) struct WindowRegistry { ids: HashMap, + by_id: HashMap, + surface_ids: HashMap, pub(super) records: HashMap, + // Ascending IDs; overview reverses this for front-to-back traversal. + overview_order: Vec, next_id: u64, } @@ -40,7 +52,10 @@ impl Default for WindowRegistry { fn default() -> Self { Self { ids: HashMap::new(), + by_id: HashMap::new(), + surface_ids: HashMap::new(), records: HashMap::new(), + overview_order: Vec::new(), next_id: 1, } } @@ -54,10 +69,16 @@ impl WindowRegistry { id } - pub fn register(&mut self, window: W, id: WindowId) { + pub fn register(&mut self, window: W, id: WindowId) + where + W: Clone, + { assert!(!self.ids.contains_key(&window), "window already registered"); assert!(!self.records.contains_key(&id), "window ID already registered"); + let index = self.overview_order.partition_point(|existing| existing.0 < id.0); + self.overview_order.insert(index, id); + self.by_id.insert(id, window.clone()); self.ids.insert(window, id); self.records.insert(id, WindowRecord::default()); } @@ -66,6 +87,24 @@ impl WindowRegistry { &self.ids } + /// Overview render and hit order, with the newest ID in front. Normal + /// stacking changes do not affect this independent presentation order. + pub fn overview_windows(&self) -> impl DoubleEndedIterator { + self.overview_order.iter().rev().filter_map(|id| self.by_id.get(id)) + } + + pub fn window(&self, id: WindowId) -> Option<&W> { + self.by_id.get(&id) + } + + pub fn id_for_surface(&self, surface: &WlSurface) -> Option { + self.surface_ids.get(surface).copied() + } + + pub fn ordered_ids(&self) -> impl DoubleEndedIterator + '_ { + self.overview_order.iter().copied() + } + pub fn record(&self, id: WindowId) -> Option<&WindowRecord> { self.records.get(&id) } @@ -74,14 +113,14 @@ impl WindowRegistry { self.records.get_mut(&id) } - pub fn records_mut(&mut self) -> impl Iterator { - self.records.iter_mut() - } - pub fn records(&self) -> impl Iterator { self.records.iter() } + pub fn records_mut(&mut self) -> impl Iterator { + self.records.iter_mut() + } + pub fn take_world_positions(&mut self) -> HashMap { self.records .iter_mut() @@ -140,7 +179,17 @@ impl WindowRegistry { pub fn remove(&mut self, window: &W) -> Option { let id = self.ids.remove(window)?; - self.records.remove(&id); + self.by_id.remove(&id); + if let Some(record) = self.records.remove(&id) + && let Some(surface) = record.surface + { + self.surface_ids.remove(&surface); + } + let index = self + .overview_order + .binary_search_by_key(&id.0, |id| id.0) + .expect("registered windows have an overview entry"); + self.overview_order.remove(index); Some(id) } @@ -153,10 +202,96 @@ impl WindowRegistry { } } +impl WindowRegistry { + pub fn register_window(&mut self, window: Window, id: WindowId) { + let surface = window.toplevel().map(|toplevel| toplevel.wl_surface().clone()); + self.register(window, id); + + if let Some(surface) = surface { + self.surface_ids.insert(surface.clone(), id); + self.records.get_mut(&id).unwrap().surface = Some(surface.clone()); + self.refresh_metadata(&surface); + } + } + + pub fn refresh_metadata(&mut self, surface: &WlSurface) -> bool { + let Some(id) = self.id_for_surface(surface) else { + return false; + }; + let record = self.records.get_mut(&id).unwrap(); + with_states(surface, |states| { + let attributes = states.data_map.get::().unwrap().lock().unwrap(); + let app_id = attributes.app_id.as_deref().unwrap_or_default(); + let title = attributes.title.as_deref().unwrap_or_default(); + let changed = record.app_id != app_id || record.title != title; + + if changed { + record.app_id.clear(); + record.app_id.push_str(app_id); + record.title.clear(); + record.title.push_str(title); + record.metadata_revision = record.metadata_revision.wrapping_add(1); + } + + changed + }) + } +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn overview_order_tracks_registration_removal_and_remapping() { + let mut windows = WindowRegistry::default(); + let first = windows.allocate_id(); + let second = windows.allocate_id(); + let third = windows.allocate_id(); + // Registration need not happen in allocation order. + windows.register("second", second); + windows.register("first", first); + windows.register("third", third); + assert_eq!( + windows.overview_windows().copied().collect::>(), + ["third", "second", "first"] + ); + + windows.remove(&"second"); + assert_eq!( + windows.overview_windows().copied().collect::>(), + ["third", "first"] + ); + windows.remove(&"first"); + let remapped = windows.allocate_id(); + windows.register("first", remapped); + assert_eq!( + windows.overview_windows().copied().collect::>(), + ["first", "third"] + ); + + windows.remove(&"first"); + windows.remove(&"third"); + assert_eq!(windows.overview_windows().count(), 0); + } + + #[test] + fn overview_iteration_borrows_handles_and_removal_releases_cached_handles() { + let handle = std::rc::Rc::new("surface"); + let mut windows = WindowRegistry::default(); + let id = windows.allocate_id(); + windows.register(handle.clone(), id); + assert_eq!(std::rc::Rc::strong_count(&handle), 3); + + for _ in 0..100 { + assert!(std::rc::Rc::ptr_eq(windows.overview_windows().next().unwrap(), &handle)); + assert_eq!(std::rc::Rc::strong_count(&handle), 3); + } + + windows.remove(&handle); + assert_eq!(std::rc::Rc::strong_count(&handle), 1); + } + #[test] fn unmap_drops_metadata_and_late_updates_cannot_resurrect_it() { let mut windows = WindowRegistry::default(); diff --git a/compositor/src/state/windows.rs b/compositor/src/state/windows.rs index d6ec3eb..1f9e7fd 100644 --- a/compositor/src/state/windows.rs +++ b/compositor/src/state/windows.rs @@ -115,7 +115,7 @@ impl Ferese { return; } - self.windows.register(window.clone(), id); + self.windows.register_window(window.clone(), id); self.windows.update(id, |w| w.placement_anchor = Some(anchor)); if rule.width.is_none() && rule.height.is_none() && client_size(&window).is_none() && remembered_work.is_none() @@ -156,7 +156,7 @@ impl Ferese { return; } - self.windows.register(window.clone(), id); + self.windows.register_window(window.clone(), id); self.window_stack.insert(id); if focus_new_window { self.focused_window = Some(id); @@ -196,7 +196,7 @@ impl Ferese { return; } - self.windows.register(window.clone(), id); + self.windows.register_window(window.clone(), id); self.window_stack.insert(id); if client_size(&window).is_none() { self.windows.update(id, |w| w.natural_floating_pending = true); @@ -649,7 +649,7 @@ impl Ferese { } pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool { - if !self.windows.ids().values().any(|window_id| *window_id == id) { + if !self.windows.window(id).is_some() { return false; } @@ -666,12 +666,7 @@ impl Ferese { } pub(super) fn send_window_close(&self, id: WindowId) { - let Some(toplevel) = self - .windows - .ids() - .iter() - .find_map(|(window, window_id)| (*window_id == id).then(|| window.toplevel()).flatten()) - else { + let Some(toplevel) = self.windows.window(id).and_then(|window| window.toplevel()) else { return; }; diff --git a/crates/ferese-animation/src/lib.rs b/crates/ferese-animation/src/lib.rs index 387dd38..2fdce03 100644 --- a/crates/ferese-animation/src/lib.rs +++ b/crates/ferese-animation/src/lib.rs @@ -234,6 +234,10 @@ impl ClientGeometry { matches_target } + pub fn waiting_for_commit(&self) -> bool { + self.waiting_since.is_some() + } + pub fn timed_out(&self, now: Duration) -> bool { self.waiting_since .is_some_and(|started| now.saturating_sub(started) >= CLIENT_COMMIT_TIMEOUT) diff --git a/crates/ferese-core/src/focus.rs b/crates/ferese-core/src/focus.rs index cdf1c35..e15a65f 100644 --- a/crates/ferese-core/src/focus.rs +++ b/crates/ferese-core/src/focus.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use ferese_layout::WindowId; /// Most recent focus first. Layer surfaces and temporary selections do not enter this list. @@ -19,19 +21,19 @@ impl FocusHistory { self.windows.retain(|id| *id != window); } - pub fn candidates(&self, current: Option, mut available: Vec) -> Vec { - available.sort_unstable_by_key(|id| id.0); - available.dedup(); - let mut ordered = Vec::with_capacity(available.len()); - for id in current - .into_iter() - .chain(self.windows.iter().copied()) - .chain(available.iter().copied()) - { - if available.contains(&id) && !ordered.contains(&id) { + pub fn candidates(&self, current: Option, available: impl AsRef<[WindowId]>) -> Vec { + let mut remaining = available.as_ref().iter().copied().collect::>(); + let mut ordered = Vec::with_capacity(remaining.len()); + + for id in current.into_iter().chain(self.windows.iter().copied()) { + if remaining.remove(&id) { ordered.push(id); } } + + let mut tail = remaining.into_iter().collect::>(); + tail.sort_unstable_by_key(|id| id.0); + ordered.extend(tail); ordered } } @@ -62,6 +64,8 @@ impl FocusCycle { } pub fn advance(&mut self, reverse: bool, available: &[WindowId]) -> Option { + let available = available.iter().copied().collect::>(); + // Keep the old index when the selected window disappears so forward // traversal selects its successor rather than skipping over it. let old = self @@ -91,6 +95,41 @@ impl FocusCycle { mod tests { use super::*; + #[test] + fn indexed_candidates_preserve_linear_order_with_duplicates_and_missing_windows() { + let mut history = FocusHistory::default(); + + for id in [7, 2, 5, 9, 2] { + history.record(WindowId(id)); + } + + for mask in 0..1024 { + let available = (0..10) + .filter(|id| mask & (1 << id) != 0) + .flat_map(|id| [WindowId(id), WindowId(id)]) + .collect::>(); + + for current in [None, Some(WindowId(2)), Some(WindowId(99))] { + let mut sorted = available.clone(); + sorted.sort_unstable_by_key(|id| id.0); + sorted.dedup(); + let mut expected = Vec::new(); + + for id in current + .into_iter() + .chain(history.windows.iter().copied()) + .chain(sorted.iter().copied()) + { + if sorted.contains(&id) && !expected.contains(&id) { + expected.push(id); + } + } + + assert_eq!(history.candidates(current, &available), expected); + } + } + } + #[test] fn history_is_unique_filters_unavailable_and_last_focus_toggles() { let mut history = FocusHistory::default(); diff --git a/crates/ferese-core/src/output.rs b/crates/ferese-core/src/output.rs index 067c46e..1f26f20 100644 --- a/crates/ferese-core/src/output.rs +++ b/crates/ferese-core/src/output.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::error::Error; use std::fmt; @@ -60,7 +60,7 @@ struct OutputState { geometry: OutputGeometry, active: WorkspaceId, previous: Option, - workspaces: HashSet, + workspaces: BTreeSet, } impl OutputState { @@ -87,7 +87,7 @@ struct EvacuationRecord { #[derive(Debug, Default)] pub struct OutputWorkspaceMap { - outputs: HashMap, + outputs: BTreeMap, assignments: HashMap, revisions: HashMap, evacuations: HashMap, @@ -95,6 +95,13 @@ pub struct OutputWorkspaceMap { } impl OutputWorkspaceMap { + pub fn assigned_workspaces(&self, output: OutputId) -> impl DoubleEndedIterator + '_ { + self.outputs + .get(&output) + .into_iter() + .flat_map(|state| state.workspaces.iter().copied()) + } + pub fn focused_output(&self) -> Option { self.focused } diff --git a/crates/ferese-core/src/workspace.rs b/crates/ferese-core/src/workspace.rs index 51e2216..b2d01b2 100644 --- a/crates/ferese-core/src/workspace.rs +++ b/crates/ferese-core/src/workspace.rs @@ -7,7 +7,7 @@ use ferese_layout::{ SizeConstraints, ViewportFocusStrategy, WindowId, }; -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct WorkspaceId(pub u64); #[derive(Debug)] @@ -78,11 +78,17 @@ impl WorkspaceLayout { } } - pub fn window_ids(&self) -> Box + '_> { - match self { - Self::Scrolling(layout) => Box::new(layout.window_ids()), - Self::Tree(layout) => Box::new(layout.window_ids()), - } + pub fn window_ids(&self) -> impl Iterator + '_ { + let scrolling = match self { + Self::Scrolling(layout) => Some(layout.window_ids()), + Self::Tree(_) => None, + }; + let tree = match self { + Self::Tree(layout) => Some(layout.window_ids()), + Self::Scrolling(_) => None, + }; + + scrolling.into_iter().flatten().chain(tree.into_iter().flatten()) } pub fn insert( diff --git a/crates/ferese-core/src/workspace_policy.rs b/crates/ferese-core/src/workspace_policy.rs index 6d5c000..372291e 100644 --- a/crates/ferese-core/src/workspace_policy.rs +++ b/crates/ferese-core/src/workspace_policy.rs @@ -15,46 +15,58 @@ pub struct WorkspaceView { impl OutputWorkspaceMap { pub fn workspace_views(&self, workspaces: &WorkspaceSet) -> Vec { - let mut outputs = self.connected_outputs().collect::>(); - outputs.sort_unstable(); + self.workspace_views_iter(workspaces).collect() + } - outputs - .into_iter() - .flat_map(|output| { - self.ordered_workspaces(workspaces, output) - .into_iter() - .enumerate() - .map(move |(index, id)| { - let visible = self.active_workspace(output) == Some(id); - WorkspaceView { - id, - output, - index: index as u32 + 1, - window_count: workspaces.workspace(id).unwrap().window_count(), - visible, - focused: visible && self.focused_output() == Some(output), - } - }) + pub fn workspace_views_iter<'a>( + &'a self, + workspaces: &'a WorkspaceSet, + ) -> impl Iterator + 'a { + self.connected_outputs() + .flat_map(move |output| self.workspace_views_for_output(workspaces, output)) + } + + pub fn workspace_views_for_output<'a>( + &'a self, + workspaces: &'a WorkspaceSet, + output: OutputId, + ) -> impl Iterator + 'a { + self.ordered_workspace_ids(workspaces, output) + .enumerate() + .map(move |(index, id)| { + let visible = self.active_workspace(output) == Some(id); + + WorkspaceView { + id, + output, + index: index as u32 + 1, + window_count: workspaces.workspace(id).unwrap().window_count(), + visible, + focused: visible && self.focused_output() == Some(output), + } }) - .collect() + } + + pub fn ordered_workspace_ids<'a>( + &'a self, + workspaces: &'a WorkspaceSet, + output: OutputId, + ) -> impl DoubleEndedIterator + 'a { + self.assigned_workspaces(output) + .filter(|id| workspaces.workspace(*id).is_some()) } pub fn ordered_workspaces(&self, workspaces: &WorkspaceSet, output: OutputId) -> Vec { - let mut ids = workspaces - .iter() - .filter(|workspace| self.output_for_workspace(workspace.id) == Some(output)) - .map(|workspace| workspace.id) - .collect::>(); - ids.sort_unstable_by_key(|id| id.0); - ids + self.ordered_workspace_ids(workspaces, output).collect() } /// Out-of-range numeric selection resolves to the trailing spare. Looking /// up a gesture target never creates workspaces or changes history. pub fn workspace_at(&self, workspaces: &WorkspaceSet, output: OutputId, index: u32) -> Option { let index = index.checked_sub(1)? as usize; - let ids = self.ordered_workspaces(workspaces, output); - ids.get(index).or_else(|| ids.last()).copied() + self.ordered_workspace_ids(workspaces, output) + .nth(index) + .or_else(|| self.ordered_workspace_ids(workspaces, output).next_back()) } /// Reconcile after membership, selection, output, or transition changes. @@ -68,17 +80,15 @@ impl OutputWorkspaceMap { workspaces.activate(active).expect("output's active workspace exists"); } - let mut protected = transitions.clone(); + let mut protected = HashSet::new(); let outputs = self.connected_outputs().collect::>(); for output in outputs { - let ordered = self.ordered_workspaces(workspaces, output); + let last = self.ordered_workspace_ids(workspaces, output).next_back(); if let Some(active) = self.active_workspace(output) { protected.insert(active); } - let spare = ordered - .last() - .copied() + let spare = last .filter(|id| workspaces.workspace(*id).is_some_and(|w| w.is_empty())) .unwrap_or_else(|| { let id = workspaces.create_workspace(); @@ -91,7 +101,10 @@ impl OutputWorkspaceMap { let removed = workspaces .iter() .filter(|workspace| { - workspace.is_empty() && !protected.contains(&workspace.id) && workspace.id != workspaces.active_id() + workspace.is_empty() + && !protected.contains(&workspace.id) + && !transitions.contains(&workspace.id) + && workspace.id != workspaces.active_id() }) .map(|workspace| workspace.id) .collect::>();