diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 39b1658..8e3e8ac 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -37,7 +37,7 @@ use smithay::wayland::presentation::Refresh; use crate::Ferese; use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform}; use crate::metrics::RenderMetrics; -use crate::render::{animated_window_elements, cursorless_window_elements, frame_effect_metrics, redraw_output}; +use crate::render::{frame_effect_metrics, redraw_output, sampled_output_elements}; pub struct DirectBackendState { pub session: LibSeatSession, @@ -793,13 +793,12 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr .scheduler .forecast_time(now, plan) .map_or(Duration::ZERO, |target| target.saturating_sub(now)); - let mut forecast = state.forecast_render(&output.output, horizon); + let frame = state.sample_frame(&output.output, horizon); let rendered = { - let state = &mut *forecast; (|| -> Result> { state.process_dmabuf_imports(&mut device.renderer); let (mut buffer, age) = output.surface.next_buffer()?; - let elements = animated_window_elements(state, &mut device.renderer, &output.output); + let elements = sampled_output_elements(state, &mut device.renderer, &output.output, true, &frame); let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale()); let mut framebuffer = device.renderer.bind(&mut buffer)?; let result = output.damage_tracker.render_output( @@ -814,7 +813,8 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true); if cursorless_capture { - let cursorless_elements = cursorless_window_elements(state, &mut device.renderer, &output.output); + let cursorless_elements = + sampled_output_elements(state, &mut device.renderer, &output.output, false, &frame); redraw_output( &mut device.renderer, &mut framebuffer, @@ -863,7 +863,7 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr record_frame_schedule(&mut output, plan, render_started.elapsed(), submitted); if submitted && let Some(token) = output.callback_timer.take() { - forecast.loop_handle.remove(token); + state.loop_handle.remove(token); } match rendered { @@ -873,8 +873,7 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr } device.outputs.insert(crtc, output); - restore_device(&mut forecast, node, device); - drop(forecast); + restore_device(state, node, device); if !submitted { schedule_frame_callbacks(state, node, crtc, plan.present_at); } @@ -1135,7 +1134,7 @@ fn remove_device(state: &mut Ferese, node: DrmNode) { let context = device.renderer.context_id().erased(); state.wallpaper.forget_context(&context); - state.resize_snapshots.retain(|_, snapshot| snapshot.context != context); + state.render.forget_context(&context); for (crtc, mut output) in device.outputs { cancel_output_timers(&state.loop_handle, &mut output); state.display_handle.disable_global::(output.global); @@ -1348,7 +1347,8 @@ fn send_frame_callbacks(state: &mut Ferese, output: &Output) { .elements() .filter(|window| { state - .window_ids + .windows + .ids() .get(*window) .is_some_and(|id| state.window_belongs_to_output(*id, output)) }) diff --git a/compositor/src/grabs/resize_grab.rs b/compositor/src/grabs/resize_grab.rs index d723d41..b824cb0 100644 --- a/compositor/src/grabs/resize_grab.rs +++ b/compositor/src/grabs/resize_grab.rs @@ -159,10 +159,9 @@ impl PointerGrab for ResizeSurfaceGrab { self.last_size = constrained_size(self.initial_rect.size, delta, self.edges, minimum, maximum); let rect = resized_rect(self.initial_rect, self.last_size, self.edges); data.set_floating_window_geometry(&self.window, rect.loc, rect.size); - if let Some(id) = data.window_ids.get(&self.window).copied() { - data.floating_resize_anchors.insert( - id, - ( + if let Some(id) = data.windows.ids().get(&self.window).copied() { + data.windows.update(id, |w| { + w.resize_anchor = Some(( self.edges.left(), self.edges.top(), ferese_layout::Rect::new( @@ -171,8 +170,8 @@ impl PointerGrab for ResizeSurfaceGrab { initial.size.w as f64, initial.size.h as f64, ), - ), - ); + )) + }); } surface.with_pending_state(|state| { diff --git a/compositor/src/handlers/activation.rs b/compositor/src/handlers/activation.rs index e52617b..81d00cb 100644 --- a/compositor/src/handlers/activation.rs +++ b/compositor/src/handlers/activation.rs @@ -43,7 +43,7 @@ impl XdgActivationHandler for Ferese { return; }; - self.focused_window = self.window_ids.get(&window).copied(); + self.focused_window = self.windows.ids().get(&window).copied(); if let Some(focused) = self.focused_window && let Err(error) = self.workspaces.focus_window(focused) { diff --git a/compositor/src/handlers/compositor.rs b/compositor/src/handlers/compositor.rs index 706abf3..b8a6338 100644 --- a/compositor/src/handlers/compositor.rs +++ b/compositor/src/handlers/compositor.rs @@ -81,7 +81,7 @@ impl CompositorHandler for Ferese { if surface == &root { if crate::state::window_has_buffer(&window) { xdg_shell::apply_initial_window_rules(self, &window); - } else if self.window_ids.contains_key(&window) { + } else if self.windows.ids().contains_key(&window) { self.remove_tiled_window(&window); self.space.map_element(window.clone(), (0, 0), false); self.restore_keyboard_focus(); diff --git a/compositor/src/handlers/mod.rs b/compositor/src/handlers/mod.rs index 336ab40..3bc36a5 100644 --- a/compositor/src/handlers/mod.rs +++ b/compositor/src/handlers/mod.rs @@ -60,7 +60,7 @@ impl SeatHandler for Ferese { fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { if let Some(id) = focused.and_then(|surface| { - self.window_ids.iter().find_map(|(window, id)| { + self.windows.ids().iter().find_map(|(window, id)| { window .toplevel() .is_some_and(|toplevel| toplevel.wl_surface() == surface) @@ -152,7 +152,7 @@ impl Ferese { { root = parent; } - let window = self.window_ids.iter().find_map(|(window, id)| { + let window = self.windows.ids().iter().find_map(|(window, id)| { window .toplevel() .is_some_and(|toplevel| toplevel.wl_surface() == &root) diff --git a/compositor/src/handlers/window_capture.rs b/compositor/src/handlers/window_capture.rs index dd922a6..afc5e99 100644 --- a/compositor/src/handlers/window_capture.rs +++ b/compositor/src/handlers/window_capture.rs @@ -186,7 +186,8 @@ impl Ferese { return None; } let window = self - .window_ids + .windows + .ids() .iter() .find(|(_, candidate)| **candidate == id)? .0 @@ -283,7 +284,7 @@ pub(super) fn copy_snapshot(state: &Ferese, resource: &ZwlrScreencopyFrameV1, da }; if state.session_lock.active || !super::screencopy::capture_allowed() - || !state.window_ids.values().any(|id| *id == snapshot.id) + || !state.windows.ids().values().any(|id| *id == snapshot.id) { resource.failed(); return; diff --git a/compositor/src/handlers/xdg_shell.rs b/compositor/src/handlers/xdg_shell.rs index 38117fd..532ccf1 100644 --- a/compositor/src/handlers/xdg_shell.rs +++ b/compositor/src/handlers/xdg_shell.rs @@ -36,7 +36,7 @@ impl XdgShellHandler for Ferese { let Some(parent) = surface.parent() else { return; }; - let parent = self.window_ids.iter().find_map(|(window, id)| { + let parent = self.windows.ids().iter().find_map(|(window, id)| { window .toplevel() .is_some_and(|toplevel| toplevel.wl_surface() == &parent) @@ -60,7 +60,8 @@ impl XdgShellHandler for Ferese { fn toplevel_destroyed(&mut self, surface: ToplevelSurface) { // Hidden workspace windows are managed but unmapped from Space. let Some(window) = self - .window_ids + .windows + .ids() .keys() .chain(self.space.elements()) .find(|window| { @@ -185,7 +186,7 @@ impl XdgShellHandler for Ferese { } fn maximize_request(&mut self, surface: ToplevelSurface) { - if let Some(id) = self.window_ids.iter().find_map(|(window, id)| { + if let Some(id) = self.windows.ids().iter().find_map(|(window, id)| { window .toplevel() .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) @@ -198,7 +199,7 @@ impl XdgShellHandler for Ferese { } fn unmaximize_request(&mut self, surface: ToplevelSurface) { - if let Some(id) = self.window_ids.iter().find_map(|(window, id)| { + if let Some(id) = self.windows.ids().iter().find_map(|(window, id)| { window .toplevel() .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) @@ -215,7 +216,7 @@ impl XdgShellHandler for Ferese { surface: ToplevelSurface, _output: Option, ) { - if let Some(window) = self.window_ids.iter().find_map(|(window, id)| { + if let Some(window) = self.windows.ids().iter().find_map(|(window, id)| { window .toplevel() .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) @@ -228,7 +229,7 @@ impl XdgShellHandler for Ferese { } fn unfullscreen_request(&mut self, surface: ToplevelSurface) { - if let Some(window) = self.window_ids.iter().find_map(|(window, id)| { + if let Some(window) = self.windows.ids().iter().find_map(|(window, id)| { window .toplevel() .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) @@ -380,7 +381,7 @@ pub fn apply_initial_window_rules(state: &mut Ferese, window: &Window) { ) }); - let initial = !state.window_ids.contains_key(window); + let initial = !state.windows.ids().contains_key(window); let requested = initial.then(|| { toplevel.with_pending_state(|pending| { ( @@ -391,7 +392,7 @@ pub fn apply_initial_window_rules(state: &mut Ferese, window: &Window) { }); if initial { let parent = toplevel.parent().and_then(|parent| { - state.window_ids.iter().find_map(|(candidate, id)| { + state.windows.ids().iter().find_map(|(candidate, id)| { candidate .toplevel() .is_some_and(|surface| surface.wl_surface() == &parent) @@ -402,7 +403,7 @@ pub fn apply_initial_window_rules(state: &mut Ferese, window: &Window) { } state.apply_initial_window_rules(window, app_id.as_deref(), title.as_deref(), transient); if let Some((maximized, fullscreen)) = requested - && let Some(id) = state.window_ids.get(window).copied() + && let Some(id) = state.windows.ids().get(window).copied() { if maximized { state.set_window_maximized(id, true); @@ -424,7 +425,7 @@ impl Ferese { .find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root)); let geometry = (|| { if let Some(window) = window { - let id = *self.window_ids.get(window)?; + let id = *self.windows.ids().get(window)?; let output = self .space .outputs() diff --git a/compositor/src/input.rs b/compositor/src/input.rs index 1992074..6a8775b 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -43,7 +43,7 @@ impl Ferese { let Some(local) = surface_tree_position(root, surface, geometry.loc) else { continue; }; - let id = self.window_ids.get(window)?; + let id = self.windows.ids().get(window)?; let presented = self.presented_window_rect(*id)?; let (scale_x, scale_y) = self.visual_scale_for_window(window)?; let point = local.to_f64() + hint - geometry.loc.to_f64(); @@ -541,7 +541,7 @@ impl Ferese { return; } if let Some(window) = self.window_under_visual(position) - && let Some(id) = self.window_ids.get(&window).copied() + && let Some(id) = self.windows.ids().get(&window).copied() { self.select_overview_window(id); } else { @@ -836,7 +836,7 @@ impl Ferese { } if let Some(window) = self.window_under_visual(position) { - let focused = self.window_ids.get(&window).copied(); + let focused = self.windows.ids().get(&window).copied(); if let Some(focused) = focused { let result = if raise { self.workspaces.focus_window(focused) @@ -904,7 +904,7 @@ impl Ferese { let Some(window) = self.window_under_visual(position) else { return; }; - if self.window_ids.get(&window).copied() == self.focused_window { + if self.windows.ids().get(&window).copied() == self.focused_window { return; } diff --git a/compositor/src/ipc.rs b/compositor/src/ipc.rs index 9a3f81f..bb5f979 100644 --- a/compositor/src/ipc.rs +++ b/compositor/src/ipc.rs @@ -827,7 +827,8 @@ impl Ferese { return; }; let Some(window) = self - .window_ids + .windows + .ids() .iter() .find(|(_, candidate)| **candidate == id) .map(|(window, _)| window.clone()) @@ -936,7 +937,8 @@ impl Ferese { .and_then(|client| client.get_credentials(&self.display_handle).ok()) .is_some_and(|credentials| credentials.pid as u32 == pid) }; - self.window_ids + self.windows + .ids() .keys() .filter_map(|window| window.toplevel()) .any(|toplevel| belongs_to(toplevel.wl_surface())) @@ -951,7 +953,8 @@ impl Ferese { use smithay::wayland::compositor::with_states; use smithay::wayland::shell::xdg::XdgToplevelSurfaceData; let mut windows = self - .window_ids + .windows + .ids() .iter() .filter_map(|(window, id)| { let toplevel = window.toplevel()?; diff --git a/compositor/src/overview.rs b/compositor/src/overview.rs index 475b10c..1221662 100644 --- a/compositor/src/overview.rs +++ b/compositor/src/overview.rs @@ -89,12 +89,9 @@ type OverviewStateLabel = #[derive(Debug)] pub(crate) struct OverviewState { - active: bool, + motion: OverviewMotion, selected: Option, selection_workspace: Option, - presentations: HashMap, - opacity: AnimatedValue, - exit_transition: Option<(f64, f64)>, strip_offsets: HashMap, font_family: String, font_requests: Option, @@ -102,7 +99,9 @@ pub(crate) struct OverviewState { labels: OverviewStateLabel, } -pub(crate) struct OverviewAnimationSnapshot { +#[derive(Clone, Debug)] +pub(crate) struct OverviewMotion { + active: bool, presentations: HashMap, opacity: AnimatedValue, exit_transition: Option<(f64, f64)>, @@ -111,12 +110,9 @@ pub(crate) struct OverviewAnimationSnapshot { impl Default for OverviewState { fn default() -> Self { Self { - active: false, + motion: OverviewMotion::default(), selected: None, selection_workspace: None, - presentations: HashMap::new(), - opacity: AnimatedValue::new(0.0), - exit_transition: None, strip_offsets: HashMap::new(), font_family: "sans-serif".into(), font_requests: None, @@ -167,27 +163,17 @@ fn centered_row_start(strip: Rect, count: usize, card_width: f64) -> f64 { impl OverviewState { pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool { - self.exit_transition.is_some() - || (self.opacity.current - self.opacity.target).abs() > 0.001 - || self.opacity.velocity.abs() > 0.005 - || self.presentations.values().any(|rect| !rect.is_settled(spring)) + self.motion.is_animating(spring) } - pub(crate) fn predict(&mut self, delta: Duration, spring: SpringConfig) -> OverviewAnimationSnapshot { - let snapshot = OverviewAnimationSnapshot { - presentations: self.presentations.clone(), - opacity: self.opacity, - exit_transition: self.exit_transition, - }; + pub(crate) fn sample(&self, delta: Duration, spring: SpringConfig) -> OverviewMotion { + let mut motion = self.motion.clone(); - self.advance(delta, spring, true); - snapshot - } + if !delta.is_zero() { + motion.advance(delta, spring, true); + } - pub(crate) fn restore_prediction(&mut self, snapshot: OverviewAnimationSnapshot) { - self.presentations = snapshot.presentations; - self.opacity = snapshot.opacity; - self.exit_transition = snapshot.exit_transition; + motion } pub(crate) fn set_font_family(&mut self, family: String) { @@ -271,19 +257,20 @@ impl OverviewState { } pub(crate) fn is_active(&self) -> bool { - self.active + self.motion.active } pub(crate) fn is_presenting(&self) -> bool { - self.active || !self.presentations.is_empty() || self.opacity.current > 0.001 + self.motion.is_presenting() } + #[cfg(test)] pub(crate) fn opacity(&self) -> f32 { - self.opacity.current.clamp(0.0, 1.0) as f32 + self.motion.opacity() } fn select_workspace_windows(&mut self, windows: &[WindowId], preferred: Option) { - let visible = |id: &WindowId| windows.contains(id) && self.presentations.contains_key(id); + let visible = |id: &WindowId| windows.contains(id) && self.motion.presentations.contains_key(id); self.selected = self .selected .filter(visible) @@ -296,11 +283,11 @@ impl OverviewState { } pub(crate) fn has_window_preview(&self, id: WindowId) -> bool { - self.presentations.contains_key(&id) + self.motion.presentations.contains_key(&id) } pub(crate) fn select_window(&mut self, id: WindowId) -> bool { - if !self.presentations.contains_key(&id) { + if !self.motion.presentations.contains_key(&id) { return false; } self.selection_workspace = None; @@ -311,13 +298,14 @@ impl OverviewState { pub(crate) fn select_direction(&mut self, direction: Direction) -> bool { let Some(current) = self .selected - .and_then(|id| self.presentations.get(&id)) + .and_then(|id| self.motion.presentations.get(&id)) .map(|presentation| presentation.current) else { return false; }; let current_center = rect_center(current); let next = self + .motion .presentations .iter() .filter(|(id, _)| Some(**id) != self.selected) @@ -337,17 +325,7 @@ impl OverviewState { } pub(crate) fn presented_rect(&self, id: WindowId, normal: Rect) -> Rect { - let preview = self.presentations.get(&id).map_or(normal, |p| p.current); - if let Some((elapsed, _)) = self.exit_transition { - let t = 1.0 - (1.0 - (elapsed / EXIT_DURATION).clamp(0.0, 1.0)).powi(3); - return Rect::new( - preview.x + (normal.x - preview.x) * t, - preview.y + (normal.y - preview.y) * t, - preview.width + (normal.width - preview.width) * t, - preview.height + (normal.height - preview.height) * t, - ); - } - preview + self.motion.presented_rect(id, normal) } fn enter( @@ -357,23 +335,23 @@ impl OverviewState { animations_enabled: bool, ) { // Reverse a dismissal from the currently visible geometry, not the old grid. - if self.exit_transition.is_some() { + if self.motion.exit_transition.is_some() { let visible: Vec<_> = targets .iter() .map(|(id, (normal, _))| (*id, self.presented_rect(*id, *normal))) .collect(); for (id, rect) in visible { - if let Some(presentation) = self.presentations.get_mut(&id) { + if let Some(presentation) = self.motion.presentations.get_mut(&id) { *presentation = AnimatedRect::new(rect); } } } - self.exit_transition = None; + self.motion.exit_transition = None; self.selection_workspace = None; - self.active = true; - self.opacity.retarget_preserving_motion(1.0); + self.motion.active = true; + self.motion.opacity.retarget_preserving_motion(1.0); if !animations_enabled { - self.opacity.snap(); + self.motion.opacity.snap(); } self.strip_offsets.clear(); self.selected = selected @@ -383,10 +361,11 @@ impl OverviewState { } fn retarget(&mut self, targets: HashMap, animations_enabled: bool) { - self.presentations.retain(|id, _| targets.contains_key(id)); + self.motion.presentations.retain(|id, _| targets.contains_key(id)); for (id, (normal, target)) in targets { let presentation = self + .motion .presentations .entry(id) .or_insert_with(|| AnimatedRect::new(normal)); @@ -396,20 +375,23 @@ impl OverviewState { presentation.snap(); } } - if self.selected.is_none_or(|id| !self.presentations.contains_key(&id)) { - self.selected = self.presentations.keys().copied().min_by_key(|id| id.0); + if self + .selected + .is_none_or(|id| !self.motion.presentations.contains_key(&id)) + { + self.selected = self.motion.presentations.keys().copied().min_by_key(|id| id.0); } } fn exit(&mut self, normal: HashMap, animations_enabled: bool) { - self.active = false; - self.exit_transition = animations_enabled.then_some((0.0, self.opacity.current)); - self.opacity.retarget_preserving_motion(0.0); + self.motion.active = false; + self.motion.exit_transition = animations_enabled.then_some((0.0, self.motion.opacity.current)); + self.motion.opacity.retarget_preserving_motion(0.0); if !animations_enabled { - self.opacity.snap(); + self.motion.opacity.snap(); } self.selected = None; - self.presentations.retain(|id, presentation| { + self.motion.presentations.retain(|id, presentation| { let Some(target) = normal.get(id).copied() else { return false; }; @@ -422,58 +404,12 @@ impl OverviewState { }); if !animations_enabled { - self.presentations.clear(); + self.motion.presentations.clear(); } } pub(crate) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations_enabled: bool) -> bool { - if let Some((elapsed, initial_opacity)) = self.exit_transition { - let elapsed = if animations_enabled { - elapsed + delta.as_secs_f64() - } else { - EXIT_DURATION - }; - let remaining = (1.0 - elapsed / EXIT_DURATION).clamp(0.0, 1.0); - self.opacity.current = initial_opacity * remaining.powi(3); - if elapsed >= EXIT_DURATION { - self.exit_transition = None; - self.presentations.clear(); - self.opacity.snap(); - return false; - } - self.exit_transition = Some((elapsed, initial_opacity)); - return true; - } - let mut active_animation = false; - - for presentation in self.presentations.values_mut() { - if animations_enabled { - active_animation |= presentation.advance(delta, spring); - } else { - presentation.snap(); - } - } - - if !self.active && !active_animation { - self.presentations.clear(); - } - - if animations_enabled { - // Geometry's 0.1-pixel tolerance is far too coarse for opacity: - // it would abruptly drop the last ten percent of the fade. - active_animation |= self.opacity.advance( - delta, - SpringConfig { - position_tolerance: 0.001, - velocity_tolerance: 0.005, - ..spring - }, - ); - } else { - self.opacity.snap(); - } - - active_animation + self.motion.advance(delta, spring, animations_enabled) } } @@ -484,7 +420,7 @@ impl Ferese { } let selected = self .window_under_visual(point) - .and_then(|w| self.window_ids.get(&w).copied()); + .and_then(|w| self.windows.ids().get(&w).copied()); if selected.is_some() && self.overview.selected != selected { self.overview.selection_workspace = None; @@ -574,7 +510,7 @@ impl Ferese { } pub(crate) fn select_overview_window(&mut self, id: WindowId) -> bool { - if !self.overview.is_active() || !self.overview.presentations.contains_key(&id) { + if !self.overview.is_active() || !self.overview.motion.presentations.contains_key(&id) { return false; } @@ -594,7 +530,7 @@ impl Ferese { let targets = self.overview_targets(); self.overview.retarget(targets, self.animations_enabled()); if let Some(cycle) = self.focus_cycle.as_mut() { - let available = self.overview.presentations.keys().copied().collect::>(); + let available = self.overview.motion.presentations.keys().copied().collect::>(); if let Some(id) = cycle.reconcile(&available) { self.overview.select_window(id); } @@ -619,7 +555,7 @@ impl Ferese { } pub(crate) fn presented_window_rect(&self, id: WindowId) -> Option { - let normal = self.window_geometry.get(&id)?.visual.current; + let normal = self.windows.geometry(&id)?.visual.current; let mut presented = self.overview.presented_rect(id, normal); if !self.overview.is_presenting() { let (x, y) = self.workspace_slide_offset(id); @@ -630,7 +566,7 @@ impl Ferese { } pub(crate) fn inverse_presented_window_point(&self, id: WindowId, x: f64, y: f64) -> Option<(f64, f64)> { - let geometry = self.window_geometry.get(&id)?; + let geometry = self.windows.geometry(&id)?; if !self.overview.is_presenting() { let presented = self.presented_window_rect(id)?; return Some((x - presented.x, y - presented.y)); @@ -653,6 +589,14 @@ impl Ferese { } pub(crate) fn overview_workspace_cards(&self, output: &Output) -> Vec { + self.overview_workspace_cards_for_frame(output, None) + } + + pub(crate) fn overview_workspace_cards_for_frame( + &self, + output: &Output, + frame: Option<&crate::state::FrameScene>, + ) -> Vec { let Some(bounds) = self.output_bounds_for(output) else { return Vec::new(); }; @@ -698,7 +642,11 @@ impl Ferese { .chain(workspace.floating.iter().copied()) .take(4) .filter_map(|id| { - let source = self.window_geometry.get(&id)?.visual.current; + let source = frame + .and_then(|frame| frame.windows.get(&id).map(|window| &window.geometry)) + .or_else(|| self.windows.geometry(&id))? + .visual + .current; Some((id, source)) }) .collect::>(); @@ -813,11 +761,11 @@ impl Ferese { if self.focus_preview_output(*id) != Some(output_id) { return None; } - Some((*id, self.window_geometry.get(id)?.visual.current)) + Some((*id, self.windows.geometry(id)?.visual.current)) }) .collect::>(); for (id, target) in overview_layout(window_area(bounds), &windows) { - targets.insert(id, (self.window_geometry[&id].visual.current, target)); + targets.insert(id, (self.windows.geometry(&id).unwrap().visual.current, target)); } continue; } @@ -829,13 +777,13 @@ impl Ferese { .window_ids() .chain(workspace.floating.iter().copied()) .filter_map(|id| { - let normal = self.window_geometry.get(&id)?.visual.current; + let normal = self.windows.geometry(&id)?.visual.current; Some((id, normal)) }) .collect::>(); for (id, target) in overview_layout(window_area(bounds), &windows) { - let normal = self.window_geometry[&id].visual.current; + let normal = self.windows.geometry(&id).unwrap().visual.current; targets.insert(id, (normal, target)); } } @@ -844,8 +792,8 @@ impl Ferese { } fn normal_window_rects(&self) -> HashMap { - self.window_geometry - .iter() + self.windows + .geometries() .map(|(id, geometry)| (*id, geometry.visual.current)) .collect() } @@ -1042,6 +990,36 @@ mod tests { assert!(!overview.is_presenting()); } + #[test] + fn sampling_overview_exit_does_not_finish_the_live_transition() { + let mut overview = OverviewState::default(); + let id = WindowId(1); + let normal = Rect::new(0., 0., 800., 600.); + let thumbnail = Rect::new(100., 100., 200., 150.); + overview.enter(HashMap::from([(id, (normal, thumbnail))]), Some(id), false); + overview.exit(HashMap::from([(id, normal)]), true); + overview.advance(Duration::from_millis(170), SpringConfig::default(), true); + let before = overview.presented_rect(id, normal); + let opacity = overview.opacity(); + + let finished = overview.sample(Duration::from_millis(16), SpringConfig::default()); + let earlier = overview.sample(Duration::from_millis(4), SpringConfig::default()); + + assert!(!finished.is_presenting()); + assert_eq!(finished.presented_rect(id, normal), normal); + assert!(earlier.is_presenting()); + assert!(earlier.opacity() < opacity); + assert!(overview.is_presenting()); + assert_eq!(overview.presented_rect(id, normal), before); + assert_eq!(overview.opacity(), opacity); + assert_eq!( + overview + .sample(Duration::ZERO, SpringConfig::default()) + .presented_rect(id, normal), + before + ); + } + #[test] fn reversing_overview_preserves_current_opacity() { let mut overview = OverviewState::default(); @@ -1231,3 +1209,95 @@ mod tests { assert!(!overview.select_direction(Direction::Right)); } } + +impl Default for OverviewMotion { + fn default() -> Self { + Self { + active: false, + presentations: HashMap::new(), + opacity: AnimatedValue::new(0.0), + exit_transition: None, + } + } +} + +impl OverviewMotion { + pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool { + self.exit_transition.is_some() + || (self.opacity.current - self.opacity.target).abs() > 0.001 + || self.opacity.velocity.abs() > 0.005 + || self.presentations.values().any(|rect| !rect.is_settled(spring)) + } + + pub(crate) fn is_presenting(&self) -> bool { + self.active || !self.presentations.is_empty() || self.opacity.current > 0.001 + } + + pub(crate) fn opacity(&self) -> f32 { + self.opacity.current.clamp(0.0, 1.0) as f32 + } + + pub(crate) fn presented_rect(&self, id: WindowId, normal: Rect) -> Rect { + let preview = self.presentations.get(&id).map_or(normal, |p| p.current); + if let Some((elapsed, _)) = self.exit_transition { + let t = 1.0 - (1.0 - (elapsed / EXIT_DURATION).clamp(0.0, 1.0)).powi(3); + return Rect::new( + preview.x + (normal.x - preview.x) * t, + preview.y + (normal.y - preview.y) * t, + preview.width + (normal.width - preview.width) * t, + preview.height + (normal.height - preview.height) * t, + ); + } + preview + } + + pub(crate) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations_enabled: bool) -> bool { + if let Some((elapsed, initial_opacity)) = self.exit_transition { + let elapsed = if animations_enabled { + elapsed + delta.as_secs_f64() + } else { + EXIT_DURATION + }; + let remaining = (1.0 - elapsed / EXIT_DURATION).clamp(0.0, 1.0); + self.opacity.current = initial_opacity * remaining.powi(3); + if elapsed >= EXIT_DURATION { + self.exit_transition = None; + self.presentations.clear(); + self.opacity.snap(); + return false; + } + self.exit_transition = Some((elapsed, initial_opacity)); + return true; + } + let mut active_animation = false; + + for presentation in self.presentations.values_mut() { + if animations_enabled { + active_animation |= presentation.advance(delta, spring); + } else { + presentation.snap(); + } + } + + if !self.active && !active_animation { + self.presentations.clear(); + } + + if animations_enabled { + // Geometry's 0.1-pixel tolerance is far too coarse for opacity: + // it would abruptly drop the last ten percent of the fade. + active_animation |= self.opacity.advance( + delta, + SpringConfig { + position_tolerance: 0.001, + velocity_tolerance: 0.005, + ..spring + }, + ); + } else { + self.opacity.snap(); + } + + active_animation + } +} diff --git a/compositor/src/presentation.rs b/compositor/src/presentation.rs index ee25513..64aa5eb 100644 --- a/compositor/src/presentation.rs +++ b/compositor/src/presentation.rs @@ -36,7 +36,8 @@ pub(crate) fn take_output_feedback( for window in state.space.elements().filter(|window| { state - .window_ids + .windows + .ids() .get(*window) .is_some_and(|id| state.window_belongs_to_output(*id, output)) }) { diff --git a/compositor/src/render/decorations.rs b/compositor/src/render/decorations.rs index 6f7fbab..45a4872 100644 --- a/compositor/src/render/decorations.rs +++ b/compositor/src/render/decorations.rs @@ -1,8 +1,15 @@ use super::*; -pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option { +pub(super) fn rounded_clip_program( + resources: &mut RenderResources, + renderer: &mut GlesRenderer, +) -> Option { let context = renderer.context_id().erased(); - if let Some(program) = state.rounded_clip_programs.get(&context) { + if let Some(program) = resources + .contexts + .get(&context) + .and_then(|programs| programs.rounded.as_ref()) + { return Some(program.clone()); } @@ -51,11 +58,11 @@ pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRender border, shadow, }; - state.rounded_clip_programs.insert(context, programs.clone()); + resources.contexts.entry(context).or_default().rounded = Some(programs.clone()); Some(programs) } Err(error) => { - if state.rounded_clip_warnings.insert(context) { + if !std::mem::replace(&mut resources.contexts.entry(context).or_default().rounded_warned, true) { tracing::warn!(%error, "rounded-window shader unavailable; falling back to unrounded rendering"); } None @@ -65,7 +72,8 @@ pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRender #[allow(clippy::too_many_arguments)] pub(super) fn window_border_element( - state: &mut Ferese, + resources: &mut RenderResources, + theme: &crate::config::ThemeSettings, renderer: &GlesRenderer, id: ferese_layout::WindowId, geometry: Rectangle, @@ -98,17 +106,13 @@ pub(super) fn window_border_element( ), None => (color, color, [0.0; 4]), }; - let (focus_from, focus_to, focus_gradient_line) = match state.theme_settings.focus_ring_gradient { + let (focus_from, focus_to, focus_gradient_line) = match theme.focus_ring_gradient { Some(g) => ( g.from.0, g.to.0, border_gradient_line(corners.rect, mode.size, output.current_transform().invert(), g.angle), ), - None => ( - state.theme_settings.accent_color.0, - state.theme_settings.accent_color.0, - [0.0; 4], - ), + None => (theme.accent_color.0, theme.accent_color.0, [0.0; 4]), }; let parameters = BorderParameters { geometry, @@ -124,7 +128,7 @@ pub(super) fn window_border_element( focus_mix, }; let context = renderer.context_id().erased(); - let buffers = state.window_borders.entry(id).or_default(); + let buffers = &mut resources.windows.entry(id).or_default().borders; if !buffers.contexts.contains_key(&context) { let element = PixelShaderElement::new( programs.border.clone(), @@ -175,7 +179,7 @@ pub(super) fn border_uniforms(parameters: &BorderParameters) -> Vec, @@ -186,14 +190,14 @@ pub(super) fn window_tint_element( ) -> Option { if color[3] <= 0.0 { if resize_fill { - state.window_resize_fills.remove(&id); + resources.clear_tint(id, true); } else { - state.window_dims.remove(&id); + resources.clear_tint(id, false); } return None; } let mode = output.current_mode()?; - let program = material_program(state, renderer)?; + let program = material_program(resources, renderer)?; let parameters = BorderParameters { geometry, clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), @@ -219,9 +223,9 @@ pub(super) fn window_tint_element( }; let context = renderer.context_id().erased(); let buffers = if resize_fill { - state.window_resize_fills.entry(id).or_default() + &mut resources.windows.entry(id).or_default().resize_fill } else { - state.window_dims.entry(id).or_default() + &mut resources.windows.entry(id).or_default().dim }; let cached = buffers.contexts.entry(context).or_insert_with(|| CachedBorder { element: PixelShaderElement::new( @@ -251,7 +255,7 @@ pub(super) fn window_tint_element( #[allow(clippy::too_many_arguments)] pub(super) fn window_shadow_element( - state: &mut Ferese, + resources: &mut RenderResources, renderer: &GlesRenderer, id: ferese_layout::WindowId, geometry: Rectangle, @@ -287,7 +291,7 @@ pub(super) fn window_shadow_element( color, }; let context = renderer.context_id().erased(); - let buffers = state.window_shadows.entry(id).or_default(); + let buffers = &mut resources.windows.entry(id).or_default().shadow; if !buffers.contexts.contains_key(&context) { let element = PixelShaderElement::new( programs.shadow.clone(), diff --git a/compositor/src/render/materials.rs b/compositor/src/render/materials.rs index 0bf912e..a419295 100644 --- a/compositor/src/render/materials.rs +++ b/compositor/src/render/materials.rs @@ -45,14 +45,14 @@ pub(super) fn append_material_surface( ) }) .collect(); - if let Some(buffers) = state.material_buffers.get_mut(surface) { + if let Some(buffers) = state.render.surfaces.get_mut(surface) { buffers.contexts.retain(|(_, index), _| *index < targets.len()); } let material = (!materials.is_empty() && regions.is_none()).then_some(()); let clip = material.as_ref().and_then(|_| { let corners = targets.first()?.1; let mode = output.current_mode()?; - let program = rounded_clip_program(state, renderer)?; + let program = rounded_clip_program(&mut state.render, renderer)?; Some(( program, framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), @@ -195,11 +195,13 @@ pub(super) fn material_element( visible_framebuffer: framebuffer_clip_rect(corners.rect, mode.size, transform), }; let context = renderer.context_id().erased(); - let program = material_program(state, renderer)?; - let blur_program = (blur > 0.0).then(|| blur_program(state, renderer)).flatten(); + let program = material_program(&mut state.render, renderer)?; + let blur_program = (blur > 0.0) + .then(|| blur_program(&mut state.render, renderer)) + .flatten(); let capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer); let capture_size = Size::from((capture_rect[2], capture_rect[3])); - let buffers = state.material_buffers.entry(surface.clone()).or_default(); + let buffers = state.render.surfaces.entry(surface.clone()).or_default(); let capture = if let Some(blur_program) = blur_program { if buffers .captures @@ -317,15 +319,19 @@ pub(super) fn material_blur_radius(style: crate::config::MaterialStyle, opacity: } } -pub(super) fn blur_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option { +pub(super) fn blur_program(resources: &mut RenderResources, renderer: &mut GlesRenderer) -> Option { let context = renderer.context_id().erased(); - if let Some(program) = state.blur_programs.get(&context) { + if let Some(program) = resources + .contexts + .get(&context) + .and_then(|programs| programs.blur.as_ref()) + { return Some(program.clone()); } match renderer.compile_custom_texture_shader(corner_shader(BLUR_SHADER), &blur_uniform_names()) { Ok(program) => { let program = BlurProgram(program); - state.blur_programs.insert(context, program.clone()); + resources.contexts.entry(context).or_default().blur = Some(program.clone()); Some(program) } Err(error) => { @@ -380,9 +386,16 @@ pub(super) fn expanded_blur_region( ) } -pub(super) fn material_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option { +pub(super) fn material_program( + resources: &mut RenderResources, + renderer: &mut GlesRenderer, +) -> Option { let context = renderer.context_id().erased(); - if let Some(program) = state.material_programs.get(&context) { + if let Some(program) = resources + .contexts + .get(&context) + .and_then(|programs| programs.material.as_ref()) + { return Some(program.clone()); } @@ -397,7 +410,7 @@ pub(super) fn material_program(state: &mut Ferese, renderer: &mut GlesRenderer) match renderer.compile_custom_pixel_shader(corner_shader(MATERIAL_SHADER), &uniforms) { Ok(program) => { let program = MaterialProgram(program); - state.material_programs.insert(context, program.clone()); + resources.contexts.entry(context).or_default().material = Some(program.clone()); Some(program) } Err(error) => { diff --git a/compositor/src/render/mod.rs b/compositor/src/render/mod.rs index 00ddeec..31f3454 100644 --- a/compositor/src/render/mod.rs +++ b/compositor/src/render/mod.rs @@ -2,8 +2,8 @@ mod capture; mod decorations; mod materials; mod overview; +mod resources; mod scene; - use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -13,9 +13,10 @@ pub(crate) use capture::{capture_resize_snapshot, capture_window_buffer, capture use decorations::*; use materials::*; use overview::*; +pub(crate) use resources::RenderResources; use scene::cursor_elements; pub(crate) use scene::{ - animated_window_elements, cursorless_window_elements, frame_effect_metrics, layer_surfaces, output_elements, + animated_window_elements, frame_effect_metrics, layer_surfaces, output_elements, sampled_output_elements, }; use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement; diff --git a/compositor/src/render/overview.rs b/compositor/src/render/overview.rs index 2a195ab..72fd8a5 100644 --- a/compositor/src/render/overview.rs +++ b/compositor/src/render/overview.rs @@ -31,9 +31,9 @@ pub(super) fn overview_chrome_element( focus_mix: 0.0, }; let program = if key.1 == OverviewChromePart::Outline { - rounded_clip_program(state, renderer)?.border + rounded_clip_program(&mut state.render, renderer)?.border } else { - material_program(state, renderer)?.0 + material_program(&mut state.render, renderer)?.0 }; let uniforms = |p: &BorderParameters| { if key.1 == OverviewChromePart::Outline { @@ -51,7 +51,8 @@ pub(super) fn overview_chrome_element( }; let context = renderer.context_id().erased(); let chrome = &mut state - .overview_scrims + .render + .outputs .entry(output_id) .or_insert_with(|| OverviewScrim { chrome: HashMap::new() }) .chrome; @@ -89,15 +90,16 @@ pub(super) fn overview_strip_elements( output: &Output, output_geometry: Rectangle, scale: f64, + frame: &crate::state::FrameScene, ) -> Vec { - let alpha = state.overview.opacity(); + let alpha = frame.overview.opacity(); if alpha <= 0.001 { return Vec::new(); } let Some(bounds) = state.output_bounds_for(output) else { return Vec::new(); }; - let cards = state.overview_workspace_cards(output); + let cards = state.overview_workspace_cards_for_frame(output, Some(frame)); let strip = crate::overview::workspace_strip(bounds); let panel = match (cards.first(), cards.last()) { (Some(first), Some(last)) => ferese_layout::Rect::new( @@ -116,16 +118,17 @@ pub(super) fn overview_strip_elements( ) .opacity; let mut elements = Vec::new(); - let programs = rounded_clip_program(state, renderer); + 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 - .window_ids + .windows + .ids() .iter() .map(|(window, id)| (*id, window.clone())) .collect(); if let Some(output_id) = state.output_id(output) - && let Some(cache) = state.overview_scrims.get_mut(&output_id) + && 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), @@ -139,7 +142,7 @@ pub(super) fn overview_strip_elements( if !state.window_belongs_to_output(*id, output) { continue; } - let Some(rect) = state.presented_window_rect(*id) else { + 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 { diff --git a/compositor/src/render/resources.rs b/compositor/src/render/resources.rs new file mode 100644 index 0000000..dede448 --- /dev/null +++ b/compositor/src/render/resources.rs @@ -0,0 +1,176 @@ +use ferese_core::OutputId; +use ferese_layout::WindowId; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; + +use super::*; + +/// Resource ownership follows the lifetime of a window, output, surface or GL +/// context. Desktop policy and animation values do not live in these caches. +#[derive(Default)] +pub(crate) struct RenderResources { + pub(super) windows: HashMap, + pub(super) contexts: HashMap, + pub(super) outputs: HashMap, + pub(super) surfaces: HashMap, +} + +#[derive(Default)] +pub(super) struct WindowBuffers { + pub borders: WindowBorderBuffers, + pub dim: WindowBorderBuffers, + pub resize_fill: WindowBorderBuffers, + pub shadow: WindowShadowBuffers, + snapshot: Option, +} + +#[derive(Default)] +pub(super) struct ContextPrograms { + pub rounded: Option, + pub rounded_warned: bool, + pub material: Option, + pub blur: Option, +} + +impl RenderResources { + pub fn remove_window(&mut self, id: WindowId) { + self.windows.remove(&id); + } + + pub fn remove_output(&mut self, id: OutputId) { + self.outputs.remove(&id); + } + + pub fn snapshot(&self, id: &WindowId) -> Option<&ResizeSnapshot> { + self.windows.get(id)?.snapshot.as_ref() + } + + pub fn snapshots(&self) -> impl Iterator { + self.windows.values().filter_map(|window| window.snapshot.as_ref()) + } + + pub fn set_snapshot(&mut self, id: WindowId, snapshot: ResizeSnapshot) { + self.windows.entry(id).or_default().snapshot = Some(snapshot); + } + + pub fn clear_snapshot(&mut self, id: &WindowId) { + if let Some(window) = self.windows.get_mut(id) { + window.snapshot = None; + } + } + + pub fn retain_snapshots(&mut self, mut keep: impl FnMut(&WindowId, &mut ResizeSnapshot) -> bool) { + for (id, window) in &mut self.windows { + if let Some(snapshot) = &mut window.snapshot + && !keep(id, snapshot) + { + window.snapshot = None; + } + } + } + + pub(super) fn clear_tint(&mut self, id: WindowId, resize_fill: bool) { + let Some(window) = self.windows.get_mut(&id) else { + return; + }; + + if resize_fill { + window.resize_fill.contexts.clear(); + } else { + window.dim.contexts.clear(); + } + } + + pub fn forget_context(&mut self, context: &ErasedContextId) { + self.contexts.remove(context); + + for window in self.windows.values_mut() { + window.borders.contexts.remove(context); + window.dim.contexts.remove(context); + window.resize_fill.contexts.remove(context); + window.shadow.contexts.remove(context); + + if window + .snapshot + .as_ref() + .is_some_and(|snapshot| &snapshot.context == context) + { + window.snapshot = None; + } + } + + for output in self.outputs.values_mut() { + for contexts in output.chrome.values_mut() { + contexts.remove(context); + } + } + + for surface in self.surfaces.values_mut() { + surface.contexts.retain(|(id, _), _| id != context); + surface.captures.remove(context); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn window_removal_releases_all_its_caches_without_removing_other_windows() { + let mut resources = RenderResources::default(); + resources.windows.insert(WindowId(1), WindowBuffers::default()); + resources.windows.insert(WindowId(2), WindowBuffers::default()); + + resources.remove_window(WindowId(1)); + resources.clear_snapshot(&WindowId(1)); + resources.clear_tint(WindowId(1), true); + + assert!(!resources.windows.contains_key(&WindowId(1))); + assert!(resources.windows.contains_key(&WindowId(2))); + } + + #[test] + #[ignore = "requires an EGL rendering device"] + fn context_removal_evicts_programs_and_snapshots_without_touching_another_gpu() { + use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; + + let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); + let display = unsafe { EGLDisplay::new(device).unwrap() }; + let mut first = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; + let mut second = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; + let first_id = first.context_id().erased(); + let second_id = second.context_id().erased(); + let mut resources = RenderResources::default(); + + for (id, renderer) in [(WindowId(1), &mut first), (WindowId(2), &mut second)] { + rounded_clip_program(&mut resources, renderer).unwrap(); + material_program(&mut resources, renderer).unwrap(); + blur_program(&mut resources, renderer).unwrap(); + let texture = Offscreen::::create_buffer(renderer, Fourcc::Abgr8888, (16, 16).into()).unwrap(); + resources.set_snapshot( + id, + ResizeSnapshot { + texture, + context: renderer.context_id().erased(), + id: Id::new(), + commit: CommitCounter::default(), + elapsed: Duration::ZERO, + last_tick: None, + scale: 1.0, + }, + ); + } + + resources.forget_context(&first_id); + + assert!(!resources.contexts.contains_key(&first_id)); + assert!(resources.contexts.contains_key(&second_id)); + assert!(resources.snapshot(&WindowId(1)).is_none()); + assert!(resources.snapshot(&WindowId(2)).is_some()); + assert_eq!(resources.snapshots().count(), 1); + + // Reconnecting a context can build fresh resources after eviction. + rounded_clip_program(&mut resources, &mut first).unwrap(); + assert!(resources.contexts.contains_key(&first_id)); + } +} diff --git a/compositor/src/render/scene.rs b/compositor/src/render/scene.rs index d5a6604..6641f89 100644 --- a/compositor/src/render/scene.rs +++ b/compositor/src/render/scene.rs @@ -8,14 +8,6 @@ pub(crate) fn animated_window_elements( output_elements(state, renderer, output, true) } -pub(crate) fn cursorless_window_elements( - state: &mut Ferese, - renderer: &mut GlesRenderer, - output: &Output, -) -> Vec { - output_elements(state, renderer, output, false) -} - pub(crate) fn frame_effect_metrics(_elements: &[AnimatedWindowRenderElement], _scale: f64) -> FrameEffectMetrics { FrameEffectMetrics } @@ -25,9 +17,20 @@ pub(crate) fn output_elements( renderer: &mut GlesRenderer, output: &Output, include_cursor: bool, +) -> Vec { + let frame = state.sample_frame(output, Duration::ZERO); + + sampled_output_elements(state, renderer, output, include_cursor, &frame) +} + +pub(crate) fn sampled_output_elements( + state: &mut Ferese, + renderer: &mut GlesRenderer, + output: &Output, + include_cursor: bool, + frame: &crate::state::FrameScene, ) -> Vec { if state.session_lock.active { - state.configure_lock_surfaces(); let Some(geometry) = state.space.output_geometry(output) else { return Vec::new(); }; @@ -83,29 +86,36 @@ pub(crate) fn output_elements( return Vec::new(); }; let scale = output.current_scale().fractional_scale(); - let rounded_clip_program = rounded_clip_program(state, renderer); + let rounded_clip_program = rounded_clip_program(&mut state.render, renderer); let mut elements = if include_cursor { cursor_elements(state, renderer, output_geometry, scale) } else { Vec::new() }; - let upper_layers: &[Layer] = if state.output_has_fullscreen(output) { + let upper_layers: &[Layer] = if state.output_has_fullscreen_for_frame(output, frame.overview.is_presenting()) { &[Layer::Overlay] } else { &[Layer::Overlay, Layer::Top] }; elements.extend(layer_elements(state, renderer, output, upper_layers)); - if state.overview.is_presenting() { - elements.extend(overview_strip_elements(state, renderer, output, output_geometry, scale)); + if frame.overview.is_presenting() { + elements.extend(overview_strip_elements( + state, + renderer, + output, + output_geometry, + scale, + frame, + )); } let mut candidates = if state.overview.is_active() { - state.window_ids.keys().cloned().collect::>() + state.windows.ids().keys().cloned().collect::>() } else { state.space.elements().rev().cloned().collect::>() }; if state.overview.is_active() { - candidates.sort_by_key(|window| std::cmp::Reverse(state.window_ids.get(window).map_or(0, |id| id.0))); + candidates.sort_by_key(|window| std::cmp::Reverse(state.windows.ids().get(window).map_or(0, |id| id.0))); } let windows = candidates .iter() @@ -115,20 +125,17 @@ pub(crate) fn output_elements( if !state.window_content_ready(window) { return None; } - let id = *state.window_ids.get(window)?; + 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 visual = state.presented_window_rect(id)?; - let (close_scale, close_alpha) = state.closing_visual(id); - let visual = scaled_visual_rect(visual, close_scale); - let decoration_progress = state - .window_geometry - .get(&id) - .map_or(1.0, |geometry| geometry.decorations.clamp(0.0, 1.0)); + 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)) }) @@ -151,12 +158,12 @@ pub(crate) fn 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 = state.overview.is_presenting() || state.closing_visual(id).0 != 1.0; + let scale_content = frame.overview.is_presenting() || frame.windows[&id].close_scale != 1.0; let behavior = resize_content_behavior(scale_content); - let dim = state.window_dimming.get(&id).map_or(0.0, |dim| dim.current); + let dim = frame.windows[&id].dim; if let Some(overlay) = window_tint_element( - state, + &mut state.render, renderer, id, constrain, @@ -175,22 +182,15 @@ pub(crate) fn 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 focused = if state.overview.is_active() { - state.overview_selected(id) - } else { - state.focused_window == Some(id) - }; - let focus = state - .window_focus - .get(&id) - .map_or(if focused { 1.0 } else { 0.0 }, |v| v.current); + let focus = frame.windows[&id].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; let gradient = state.theme_settings.border_gradient; if let Some(border) = window_border_element( - state, + &mut state.render, + &state.theme_settings, renderer, id, constrain, @@ -207,7 +207,7 @@ pub(crate) fn output_elements( elements.push(border.into()); } let shadow = window_shadow_element( - state, + &mut state.render, renderer, id, constrain, @@ -221,7 +221,7 @@ pub(crate) fn output_elements( &programs, ); if !scale_content - && let Some(snapshot) = state.resize_snapshots.get(&id) + && let Some(snapshot) = state.render.snapshot(&id) && snapshot.context == renderer.context_id().erased() && (snapshot.scale - scale).abs() < 0.001 { @@ -260,10 +260,7 @@ pub(crate) fn output_elements( corners, scale, close_alpha, - state - .window_geometry - .get(&id) - .is_some_and(|geometry| geometry.presentation_changed), + frame.windows[&id].geometry.presentation_changed, output, programs.clone(), behavior, @@ -275,13 +272,15 @@ pub(crate) fn output_elements( { let mut color = state.theme_settings.surface_base_color.0; color[3] = close_alpha; - if let Some(fill) = window_tint_element(state, renderer, id, constrain, corners, color, true, output) { + if let Some(fill) = + window_tint_element(&mut state.render, renderer, id, constrain, corners, color, true, output) + { // Front-to-back: fill uncovered strips behind the native // content instead of stretching it or exposing wallpaper. elements.push(fill.into()); } } else { - state.window_resize_fills.remove(&id); + state.render.clear_tint(id, true); } if let Some(surface) = material_surface && let Some((background, _)) = material_element( @@ -370,7 +369,7 @@ pub(super) fn layer_elements( .collect::>() }; - state.material_buffers.retain(|surface, _| surface.is_alive()); + state.render.surfaces.retain(|surface, _| surface.is_alive()); let mut elements = Vec::new(); for (geometry, layer) in layers { diff --git a/compositor/src/shell_control.rs b/compositor/src/shell_control.rs index 7fa48f1..7876879 100644 --- a/compositor/src/shell_control.rs +++ b/compositor/src/shell_control.rs @@ -338,7 +338,8 @@ impl Ferese { fn managed_window_snapshots(&self) -> Vec { let mut snapshots = self - .window_ids + .windows + .ids() .iter() .filter_map(|(window, id)| { let workspace = self.workspaces.workspace_for_window(*id)?.0; diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 1a5863b..a249b99 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -6,9 +6,10 @@ mod layout; mod navigation; mod outputs; mod prediction; +pub(crate) use prediction::FrameScene; +mod window_registry; mod windows; -use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::error::Error; use std::ffi::OsString; @@ -28,7 +29,7 @@ use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::drm::{DrmEventTime, DrmNode}; use smithay::backend::input::Keycode; -use smithay::backend::renderer::{ErasedContextId, ImportDma}; +use smithay::backend::renderer::ImportDma; use smithay::desktop::space::SpaceElement; use smithay::desktop::utils::send_frames_surface_tree; use smithay::desktop::{LayerSurface, PopupKind, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output}; @@ -71,6 +72,7 @@ use smithay::wayland::viewporter::ViewporterState; use smithay::wayland::xdg_activation::XdgActivationState; use smithay::wayland::xdg_foreign::XdgForeignState; use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconManager; +use window_registry::WindowRegistry; use crate::backends::direct::DirectBackendState; use crate::config::{Binding, DaemonConfig, InputSettings, OutputProfile, ThemeSettings}; @@ -86,11 +88,6 @@ use crate::ipc::IpcSocketGuard; use crate::overview::OverviewState; use crate::portal_session::PortalSession; use crate::portal_shortcuts::PortalShortcuts; -use crate::render::{ - BlurProgram, MaterialBuffers, MaterialProgram, OverviewScrim, ResizeSnapshot, RoundedClipPrograms, - WindowBorderBuffers, WindowShadowBuffers, -}; -use crate::resize_transaction::ResizeTransaction; use crate::session_lock::{IdleSettings, Lock}; use crate::shell_control::ShellSnapshot; use crate::stacking::WindowStack; @@ -326,47 +323,23 @@ pub struct Ferese { pub output_workspaces: OutputWorkspaceMap, pub(crate) output_ids: HashMap, output_identity_ids: HashMap, - pub window_ids: HashMap, - pub window_geometry: HashMap, - resize_transactions: HashMap, - pub(crate) resize_snapshots: HashMap, + pub(crate) windows: WindowRegistry, + pub(crate) render: crate::render::RenderResources, pub(crate) nested_backend: Option, pub(crate) wallpaper: WallpaperState, - maximized_windows: HashSet, - maximized_column_widths: HashMap, window_stack: WindowStack, floating_above_fullscreen: HashMap, - natural_floating_pending: HashSet, floating_memory: crate::floating::Memory, floating_save_worker: crate::floating::SaveWorker, - floating_window_memory: HashMap, - floating_placement_anchors: HashMap, floating_cascade: crate::floating::Cascade, - pub(crate) floating_resize_anchors: HashMap, - pub(crate) window_borders: HashMap, - pub(crate) window_dims: HashMap, - pub(crate) window_resize_fills: HashMap, - pub(crate) window_dimming: HashMap, - pub(crate) window_focus: HashMap, - pub(crate) window_shadows: HashMap, - pub(crate) rounded_clip_warnings: HashSet, - pub(crate) rounded_clip_programs: HashMap, - pub(crate) overview_scrims: HashMap, - pub(crate) material_programs: HashMap, - pub(crate) material_buffers: HashMap, pub(crate) portal_shortcuts: PortalShortcuts, pub(crate) pending_logout: Option, pub(crate) logout_query: Option, pub(crate) logout_owner: Option, - pub(crate) blur_programs: HashMap, pub(crate) backdrop_generation: u64, - closing_windows: HashMap, viewport_animations: HashMap, focus_swipe: Option, workspace_slides: HashMap, - scrolling_world_x: HashMap, - viewport_coupled_widths: HashMap, - pending_column_width_cycles: HashSet, pub focused_window: Option, pub(crate) focus_history: ferese_core::FocusHistory, pub(crate) focus_cycle: Option, @@ -376,7 +349,6 @@ pub struct Ferese { pub(crate) bindings: Vec, workspace_auto_back_and_forth: bool, window_rules: Vec, - window_rules_applied: HashSet, pub(crate) theme_settings: ThemeSettings, pub(crate) inactive_dim: crate::config::InactiveDimSettings, animations_enabled: bool, @@ -411,7 +383,6 @@ pub struct Ferese { pub(crate) shell_snapshot_serial: u32, pub(crate) last_shell_snapshot: Option, pub(crate) overview: crate::overview::OverviewState, - next_window_id: u64, next_output_id: u64, last_animation_tick: Instant, pub popups: PopupManager, @@ -559,49 +530,25 @@ impl Ferese { output_workspaces: OutputWorkspaceMap::default(), output_ids: HashMap::new(), output_identity_ids: HashMap::new(), - window_ids: HashMap::new(), - window_geometry: HashMap::new(), - resize_transactions: HashMap::new(), - resize_snapshots: HashMap::new(), + windows: WindowRegistry::default(), + render: Default::default(), nested_backend: None, wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())), - maximized_windows: HashSet::new(), - maximized_column_widths: HashMap::new(), window_stack: WindowStack::default(), floating_above_fullscreen: HashMap::new(), - natural_floating_pending: HashSet::new(), floating_memory: crate::floating::Memory::path() .map(|p| crate::floating::Memory::load(&p)) .unwrap_or_default(), floating_save_worker: Default::default(), - floating_window_memory: HashMap::new(), - floating_placement_anchors: HashMap::new(), floating_cascade: Default::default(), - floating_resize_anchors: HashMap::new(), - window_borders: HashMap::new(), - window_dims: HashMap::new(), - window_resize_fills: HashMap::new(), - window_dimming: HashMap::new(), - window_focus: HashMap::new(), - window_shadows: HashMap::new(), - rounded_clip_programs: HashMap::new(), - rounded_clip_warnings: HashSet::new(), - overview_scrims: HashMap::new(), - material_programs: HashMap::new(), - material_buffers: HashMap::new(), portal_shortcuts: PortalShortcuts::default(), pending_logout: None, logout_query: None, logout_owner: None, - blur_programs: HashMap::new(), backdrop_generation: 0, - closing_windows: HashMap::new(), viewport_animations: HashMap::new(), focus_swipe: None, workspace_slides: HashMap::new(), - scrolling_world_x: HashMap::new(), - viewport_coupled_widths: HashMap::new(), - pending_column_width_cycles: HashSet::new(), focused_window: None, focus_history: Default::default(), focus_cycle: None, @@ -611,7 +558,6 @@ impl Ferese { bindings: config.bindings, workspace_auto_back_and_forth: config.workspace_auto_back_and_forth, window_rules: config.window_rules, - window_rules_applied: HashSet::new(), theme_settings: config.theme_settings, inactive_dim: config.inactive_dim, animations_enabled: config.animations_enabled, @@ -641,7 +587,6 @@ impl Ferese { shell_snapshot_serial: 0, last_shell_snapshot: None, overview: OverviewState::with_font_family(config.overview_font_family), - next_window_id: 1, next_output_id: 1, last_animation_tick: start_time, popups: PopupManager::default(), diff --git a/compositor/src/state/animation.rs b/compositor/src/state/animation.rs index 461e9fd..c66d24d 100644 --- a/compositor/src/state/animation.rs +++ b/compositor/src/state/animation.rs @@ -29,6 +29,7 @@ impl Ferese { { self.focus_swipe = None; } + let visible_workspaces = if self.viewport_animations.is_empty() { HashSet::new() } else { @@ -38,10 +39,11 @@ impl Ferese { .filter_map(|output| self.output_workspaces.active_workspace(*output)) .collect::>() }; + let windows = self .space .elements() - .filter_map(|window| self.window_ids.get(window).copied().map(|id| (window.clone(), id))) + .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(); @@ -49,16 +51,19 @@ impl Ferese { if slide.held_progress.is_some() { continue; } + if slide.advance(delta) { active_animation = true; } else { completed_slides.push(*output); } } + self.dismissing_popups.retain_mut(|(root, popup, motion)| { if !smithay::utils::IsAlive::alive(popup.wl_surface()) { return false; } + let duration = if self.animations_enabled { 140.0 } else { 0.0 }; let active = motion.advance_visual(0.0, delta, duration); crate::effects::fade_dismissed_surface(popup.wl_surface(), motion.current); @@ -75,69 +80,72 @@ impl Ferese { } else { 0.0 }; + let mut dim_changed = false; // Include hidden workspace windows: overview can present them too. // Read selection once so these immutable fields can be borrowed alongside - // window_focus without allocating a temporary window-ID vector. + // each record without allocating a temporary window-ID vector. let selected_window = if self.overview.is_active() { self.overview.selected() } else { self.focused_window }; - for id in self.window_ids.values().copied() { + + for (&id, record) in self.windows.records_mut() { let selected = selected_window == Some(id); let target = if selected { 1.0 } else { 0.0 }; - let focus = self - .window_focus - .entry(id) - .or_insert_with(|| crate::dimming::DimAnimation::new(target)); + let focus = record + .focus + .get_or_insert_with(|| crate::dimming::DimAnimation::new(target)); let previous = focus.current; active_animation |= focus.advance_visual(target, delta, duration); 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 dim = self - .window_dimming - .entry(*id) - .or_insert_with(|| crate::dimming::DimAnimation::new(target)); + let Some(record) = self.windows.record_mut(*id) else { + continue; + }; + + let dim = record + .dimming + .get_or_insert_with(|| crate::dimming::DimAnimation::new(target)); let previous = dim.current; active_animation |= dim.advance_visual(target, delta, duration); dim_changed |= previous != dim.current; } let mut ready_to_close = Vec::new(); - for (id, animation) in &mut self.closing_windows { + for (id, record) in self.windows.records_mut() { + let Some(animation) = &mut record.closing else { continue }; + if animation.advance(delta, self.animations_enabled) { ready_to_close.push(*id); } else if !animation.close_sent { active_animation = true; } } + for id in ready_to_close { self.send_window_close(id); } let now = self.start_time.elapsed(); - self.resize_transactions.retain(|id, transaction| { - if transaction.expired(now) { - tracing::warn!(?id, "resize presentation deadline reached"); - false - } else { - true - } - }); + self.windows.expire_transactions(now); + let blocked_workspaces = self - .resize_transactions - .keys() + .windows + .resizing() .filter_map(|id| self.workspaces.workspace_for_window(*id)) .collect::>(); let animations_enabled = self.animations_enabled; let animation_speed = self.animation_speed; - self.resize_snapshots.retain(|id, snapshot| { + self.render.retain_snapshots(|id, snapshot| { if !animations_enabled { return false; } + let waiting_for_client = self .workspaces .workspace_for_window(*id) @@ -145,7 +153,7 @@ impl Ferese { // A shrinking client's destination buffer arrives before the // animated bounds reach it. Keep the old native pixels covering // that strip rather than fading them into the neutral resize fill. - let uncovered = self.window_geometry.get(id).is_some_and(|geometry| { + let uncovered = self.windows.geometry(id).is_some_and(|geometry| { geometry.client.committed_size.is_some_and(|size| { crate::presentation::resize_needs_old_frame( geometry.visual.current, @@ -166,21 +174,24 @@ impl Ferese { if !blocked { snapshot.commit.increment(); } + if !active { tracing::debug!(?id, bytes = snapshot.bytes(), "released resize handoff snapshot"); } active }); - active_animation |= !self.resize_snapshots.is_empty(); + active_animation |= self.render.snapshots().next().is_some(); // 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) { continue; } + if blocked_workspaces.contains(workspace) { continue; } + if let Some(swipe) = self.focus_swipe.as_ref().filter(|swipe| swipe.workspace == *workspace) { viewport.current = swipe.position(); viewport.velocity = 0.0; @@ -200,12 +211,19 @@ impl Ferese { { continue; } - let Some(geometry) = self.window_geometry.get_mut(&id) else { + + let Some(record) = self.windows.record_mut(id) else { continue; }; + + let natural_pending = record.natural_floating_pending; + let Some(geometry) = record.geometry.as_mut() else { + continue; + }; + let zooming = geometry.is_zooming(); - let coupled_target = self.viewport_coupled_widths.get(&id).map(|(_, width)| width.target); + let coupled_target = record.coupled_width.as_ref().map(|(_, width)| width.target); if coupled_target.is_some() { geometry.visual.target.width = geometry.visual.current.width; geometry.visual.velocity.width = 0.0; @@ -214,7 +232,8 @@ impl Ferese { if let Some(target) = coupled_target { geometry.visual.target.width = target; } - if let Some((workspace, world_x)) = self.scrolling_world_x.get_mut(&id) + + if let Some((workspace, world_x)) = record.world_x.as_mut() && let Some(viewport) = self.viewport_animations.get(workspace) { if zooming { @@ -225,12 +244,14 @@ impl Ferese { } else { world_x.snap(); } + if !zooming { geometry.visual.current.x = world_x.current - viewport.current; geometry.visual.velocity.x = world_x.velocity - viewport.velocity; } } - if let Some((_, width)) = self.viewport_coupled_widths.get_mut(&id) { + + if let Some((_, width)) = record.coupled_width.as_mut() { let width_active = if self.animations_enabled { width.advance(delta, self.viewport_spring_config) } else { @@ -244,6 +265,7 @@ impl Ferese { settled_coupled_widths.push(id); } } + if let Some(size) = geometry.client.expire_wait(now) { tracing::warn!( ?id, @@ -253,8 +275,9 @@ impl Ferese { "client did not commit the final configured size within 500 ms" ); } + if let Some(size) = geometry.presentation_size_request(now) - && !self.natural_floating_pending.contains(&id) + && !natural_pending && let Some(toplevel) = window.toplevel() { toplevel.with_pending_state(|state| { @@ -262,20 +285,25 @@ impl Ferese { }); toplevel.send_pending_configure(); } + let visual = geometry.visual.current; self.space .map_element(window, (visual.x.round() as i32, visual.y.round() as i32), false); } + for id in settled_coupled_widths { - self.viewport_coupled_widths.remove(&id); + self.windows.update(id, |record| record.coupled_width = None); } + if !completed_slides.is_empty() { for output in completed_slides { self.workspace_slides.remove(&output); } + let visible = self.visible_workspace_ids(); let visible_windows = self - .window_ids + .windows + .ids() .values() .copied() .filter(|id| { @@ -308,19 +336,15 @@ impl Ferese { scaled_animation_duration(duration, self.animations_enabled, self.animation_speed) } - pub(crate) fn closing_visual(&self, id: WindowId) -> (f64, f32) { - let Some(animation) = self.closing_windows.get(&id) else { - return (1.0, 1.0); - }; - let eased = smoothstep(animation.progress); - - (1.0 - eased * 0.02, (1.0 - eased) as f32) + pub(crate) fn workspace_slide_offset(&self, window: WindowId) -> (f64, f64) { + self.workspace_slide_offset_at(window, Duration::ZERO) } - pub(crate) fn workspace_slide_offset(&self, window: WindowId) -> (f64, f64) { + 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); }; + let Some(output_id) = self .workspace_slides .iter() @@ -329,9 +353,11 @@ impl Ferese { else { return (0.0, 0.0); }; + let Some(slide) = self.workspace_slides.get(&output_id) else { return (0.0, 0.0); }; + let Some(size) = self .output_ids .iter() @@ -341,6 +367,12 @@ impl Ferese { else { return (0.0, 0.0); }; + + let mut slide = slide.clone(); + if slide.held_progress.is_none() && !delta.is_zero() { + slide.advance(delta); + } + slide.offset(workspace, f64::from(size.w), f64::from(size.h)) } diff --git a/compositor/src/state/floating.rs b/compositor/src/state/floating.rs index 98d03a0..741c970 100644 --- a/compositor/src/state/floating.rs +++ b/compositor/src/state/floating.rs @@ -18,7 +18,8 @@ impl Ferese { None => return, }; if let Some(handle) = self - .window_ids + .windows + .ids() .iter() .find_map(|(handle, id)| (*id == window).then_some(handle.clone())) { @@ -75,7 +76,7 @@ impl Ferese { .workspace_for_window(window) .and_then(|workspace| self.workspaces.workspace(workspace)) .is_some_and(|workspace| workspace.fullscreen == Some(window)); - let enabled = fullscreen || !self.maximized_windows.contains(&window); + let enabled = fullscreen || !self.windows.record(window).is_some_and(|w| w.maximized); self.set_window_maximized(window, enabled); } @@ -85,9 +86,9 @@ impl Ferese { } if enabled { let _ = self.workspaces.set_fullscreen(window, false); - self.maximized_windows.insert(window); + self.windows.update(window, |w| w.maximized = true); } else { - self.maximized_windows.remove(&window); + self.windows.update(window, |w| w.maximized = false); } if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) { let workspace_id = self.workspaces.workspace_for_window(window).unwrap(); @@ -96,10 +97,16 @@ impl Ferese { { if enabled { if let Some(column) = layout.columns().iter().find(|column| column.windows.contains(&window)) { - self.maximized_column_widths.entry(window).or_insert(column.width); + self.windows.update(window, |w| { + w.maximized_column_width.get_or_insert(column.width); + }); } let _ = layout.set_column_width(window, ColumnWidth::Proportion(1.0)); - } else if let Some(width) = self.maximized_column_widths.remove(&window) { + } else if let Some(width) = self + .windows + .update(window, |w| w.maximized_column_width.take()) + .flatten() + { let _ = layout.set_column_width(window, width); } } @@ -108,7 +115,11 @@ impl Ferese { } pub(crate) fn output_has_fullscreen(&self, output: &Output) -> bool { - !self.overview.is_presenting() + self.output_has_fullscreen_for_frame(output, self.overview.is_presenting()) + } + + pub(crate) fn output_has_fullscreen_for_frame(&self, output: &Output, overview: bool) -> bool { + !overview && self .output_id(output) .and_then(|id| self.output_workspaces.active_workspace(id)) @@ -127,7 +138,8 @@ impl Ferese { } pub fn is_floating_window(&self, window: &Window) -> bool { - self.window_ids + self.windows + .ids() .get(window) .and_then(|id| self.workspaces.placement(*id)) .is_some_and(|placement| matches!(placement, WindowPlacement::Floating { .. })) @@ -139,10 +151,10 @@ impl Ferese { location: Point, size: Size, ) { - let Some(id) = self.window_ids.get(window).copied() else { + let Some(id) = self.windows.ids().get(window).copied() else { return; }; - self.floating_resize_anchors.remove(&id); + self.windows.update(id, |w| w.resize_anchor.take()).flatten(); let rect = Rect::new( location.x as f64, location.y as f64, @@ -157,12 +169,15 @@ impl Ferese { // Direct manipulation follows the pointer, including while the client is // still drawing its next buffer. Rendering and hit testing share this rect. - self.resize_transactions.remove(&id); - self.resize_snapshots.remove(&id); - let geometry = self - .window_geometry - .entry(id) - .or_insert_with(|| WindowGeometry::new(rect, client_size(window))); + self.windows.clear_transaction(&id); + self.render.clear_snapshot(&id); + let Some(record) = self.windows.record_mut(id) else { + return; + }; + let geometry = record + .geometry + .get_or_insert_with(|| WindowGeometry::new(rect, client_size(window))); + if let Some(size) = geometry.follow_pointer(rect, self.start_time.elapsed()) && let Some(toplevel) = window.toplevel() { @@ -171,8 +186,8 @@ impl Ferese { }); toplevel.send_pending_configure(); } - self.scrolling_world_x.remove(&id); - self.viewport_coupled_widths.remove(&id); + 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.sync_window_stacking(); // Move/resize/cancel callbacks run with Smithay's pointer mutex held. @@ -212,7 +227,7 @@ impl Ferese { self.space .elements() .filter_map(|window| { - let id = *self.window_ids.get(window)?; + let id = *self.windows.ids().get(window)?; if Some(id) == exclude { return None; } @@ -255,7 +270,7 @@ impl Ferese { let parent_rect = parent .as_ref() .and_then(|surface| { - self.window_ids.iter().find_map(|(window, id)| { + self.windows.ids().iter().find_map(|(window, id)| { window .toplevel() .is_some_and(|top| top.wl_surface() == surface) @@ -266,7 +281,7 @@ impl Ferese { .and_then(|rect| intersection(rect, work)); let dialog = crate::window_rules::is_native_dialog(app.as_deref()); let remembered = id - .and_then(|id| self.floating_window_memory.get(&id)) + .and_then(|id| self.windows.record(id).and_then(|w| w.floating_memory.as_ref())) .or_else(|| { if parent.is_none() { app.as_deref().and_then(|app| self.floating_memory.get(app)) @@ -317,7 +332,7 @@ impl Ferese { .map(|(_, r)| r) .collect::>(); let anchor = id - .and_then(|id| self.floating_placement_anchors.get(&id).copied()) + .and_then(|id| self.windows.record(id).and_then(|w| w.placement_anchor)) .or_else(|| { self.focused_window .filter(|focused| Some(*focused) != id) @@ -337,7 +352,7 @@ impl Ferese { } pub(crate) fn remember_floating(&mut self, window: &Window) { - let Some(id) = self.window_ids.get(window).copied() else { + let Some(id) = self.windows.ids().get(window).copied() else { return; }; let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id) else { @@ -360,7 +375,7 @@ impl Ferese { return; }; let entry = crate::floating::Remembered::new(output, rect, work); - self.floating_window_memory.insert(id, entry.clone()); + self.windows.update(id, |w| w.floating_memory = Some(entry.clone())); if let Some(app) = app { self.floating_memory.save(app, entry); } @@ -377,7 +392,7 @@ impl Ferese { window: &Window, raw: Rect, ) -> (Vec, Vec) { - let id = self.window_ids.get(window).copied(); + let id = self.windows.ids().get(window).copied(); let work = self .floating_outputs() .into_iter() diff --git a/compositor/src/state/focus.rs b/compositor/src/state/focus.rs index ad7aef1..d0397b3 100644 --- a/compositor/src/state/focus.rs +++ b/compositor/src/state/focus.rs @@ -2,22 +2,23 @@ use super::*; impl Ferese { pub(super) fn focus_candidates(&self) -> Vec { - self.window_ids + self.windows + .ids() .iter() .filter_map(|(window, id)| { self.workspaces.workspace_for_window(*id)?; (window.toplevel().is_some() && self.window_content_ready(window) - && self.window_geometry.contains_key(id) + && self.windows.geometry(id).is_some() && self.output_workspaces.focused_output().is_some() - && !self.closing_windows.contains_key(id)) + && !self.windows.record(*id).is_some_and(|record| record.closing.is_some())) .then_some(*id) }) .collect() } pub(crate) fn focus_preview_output(&self, id: WindowId) -> Option { - if self.closing_windows.contains_key(&id) { + if self.windows.record(id).is_some_and(|record| record.closing.is_some()) { return None; } self.workspaces diff --git a/compositor/src/state/hit_testing.rs b/compositor/src/state/hit_testing.rs index 594cfdf..4ce8e43 100644 --- a/compositor/src/state/hit_testing.rs +++ b/compositor/src/state/hit_testing.rs @@ -41,7 +41,7 @@ impl Ferese { let workspace = self.workspace_under_pointer(position)?; self.space.elements().rev().find_map(|window| { - let id = self.window_ids.get(window)?; + let id = self.windows.ids().get(window)?; if self.workspaces.workspace_for_window(*id) != Some(workspace) { return None; } @@ -71,7 +71,7 @@ impl Ferese { } pub(super) fn window_fills_visual_bounds(&self, id: WindowId, workspace: WorkspaceId) -> bool { - self.maximized_windows.contains(&id) + self.windows.record(id).is_some_and(|w| w.maximized) || self .workspaces .workspace(workspace) @@ -137,7 +137,7 @@ impl Ferese { return None; } - let id = self.window_ids.get(window)?; + let id = self.windows.ids().get(window)?; if self.workspaces.workspace_for_window(*id) != Some(workspace) { return None; } @@ -166,9 +166,9 @@ impl Ferese { }; if overview_active { - let mut candidates = self.window_ids.keys().cloned().collect::>(); + 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.window_ids.get(window).map_or(0, |id| id.0))); + candidates.sort_by_key(|window| std::cmp::Reverse(self.windows.ids().get(window).map_or(0, |id| id.0))); candidates.iter().find_map(hit) } else { self.space.elements().rev().find_map(hit) @@ -199,8 +199,8 @@ impl Ferese { if !self.overview.is_presenting() { return Some((1.0, 1.0)); } - let id = self.window_ids.get(window)?; - let geometry = self.window_geometry.get(id)?; + let id = self.windows.ids().get(window)?; + let geometry = self.windows.geometry(id)?; let source = geometry.client.committed_size?; let presented = self.presented_window_rect(*id)?; @@ -215,7 +215,7 @@ impl Ferese { } pub(crate) fn visual_rect_for_window(&self, window: &Window) -> Option> { - let id = self.window_ids.get(window)?; + let id = self.windows.ids().get(window)?; let rect = self.presented_window_rect(*id)?; let size = ClientSize::from_rect(rect); Some(Rectangle::new( @@ -227,9 +227,10 @@ impl Ferese { pub(crate) fn window_content_ready(&self, window: &Window) -> bool { window_has_buffer(window) && self - .window_ids + .windows + .ids() .get(window) - .and_then(|id| self.window_geometry.get(id)) + .and_then(|id| self.windows.geometry(id)) .is_some_and(|geometry| geometry.client.committed_size.is_some()) } } diff --git a/compositor/src/state/layout.rs b/compositor/src/state/layout.rs index f094665..b1eabd3 100644 --- a/compositor/src/state/layout.rs +++ b/compositor/src/state/layout.rs @@ -26,16 +26,25 @@ impl Ferese { pub(super) fn unmap_invisible_windows(&mut self, visible: &HashSet) -> bool { let mut changed = false; - for (window, id) in &self.window_ids { - if visible.contains(id) { + let windows = self + .windows + .ids() + .iter() + .map(|(window, id)| (window.clone(), *id)) + .collect::>(); + + for (window, id) in windows { + if visible.contains(&id) { continue; } - let was_mapped = self.space.element_location(window).is_some(); + + let was_mapped = self.space.element_location(&window).is_some(); changed |= was_mapped; - if was_mapped && let Some(geometry) = self.window_geometry.get_mut(id) { + if was_mapped && let Some(geometry) = self.windows.geometry_mut(&id) { geometry.settle_presentation(); } - self.space.unmap_elem(window); + + self.space.unmap_elem(&window); } changed } @@ -51,8 +60,8 @@ impl Ferese { // large first animation step after a keypress on an idle desktop. self.advance_animations(Instant::now()); self.arrange_layers(); - let mut previous_scrolling_world_x = std::mem::take(&mut self.scrolling_world_x); - let pending_column_width_cycles = std::mem::take(&mut self.pending_column_width_cycles); + let mut previous_scrolling_world_x = self.windows.take_world_positions(); + let pending_column_width_cycles = self.windows.take_column_width_requests(); let outputs = self.space.outputs().cloned().collect::>(); let constraints = self.window_constraints(); let mut visible = HashSet::new(); @@ -63,6 +72,7 @@ impl Ferese { let Some(output_id) = self.output_ids.get(&output).copied() else { continue; }; + let Some(active_workspace) = self.output_workspaces.active_workspace(output_id) else { continue; }; @@ -80,13 +90,16 @@ impl Ferese { let Some(bounds) = self.output_bounds_for(&output) else { continue; }; + let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds); for layer in layer_map_for_output(&output).layers() { crate::handlers::set_surface_tree_output(layer.wl_surface(), &output); } + let Some(workspace) = self.workspaces.workspace(workspace_id) else { continue; }; + let workspace_fullscreen = workspace.fullscreen; let is_scrolling_layout = matches!(workspace.layout, WorkspaceLayout::Scrolling(_)); let workspace_focus = workspace.last_focused; @@ -97,6 +110,7 @@ impl Ferese { let Some(workspace) = self.workspaces.workspace_mut(workspace_id) else { continue; }; + let layout = match workspace .layout @@ -134,15 +148,17 @@ impl Ferese { ); } - for (window, id) in &self.window_ids { + for (window, id) in self.windows.ids() { if self.workspaces.workspace_for_window(*id) != Some(workspace_id) { continue; } + if let Some(toplevel) = window.toplevel() { crate::handlers::set_surface_tree_output(toplevel.wl_surface(), &output); } - let is_maximized = self.maximized_windows.contains(id) && workspace_fullscreen != Some(*id); + let is_maximized = + self.windows.record(*id).is_some_and(|w| w.maximized) && workspace_fullscreen != Some(*id); let rect = if workspace_fullscreen == Some(*id) { fullscreen_bounds } else if is_maximized && !is_scrolling_layout { @@ -166,6 +182,7 @@ impl Ferese { None => continue, } }; + let is_fullscreen = workspace_fullscreen == Some(*id); let is_floating = matches!(self.workspaces.placement(*id), Some(WindowPlacement::Floating { .. })); @@ -195,19 +212,25 @@ impl Ferese { let now = self.start_time.elapsed(); - let mut scrolling_world_x = HashMap::new(); placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id)); for (window, id, rect, is_fullscreen, is_maximized, is_floating, scrolling, couple_width) in placements { let was_mapped = self.space.element_location(&window).is_some(); + let Some(record) = self.windows.record_mut(id) else { + continue; + }; + // A viewport-coupled width must never override fullscreen/floating geometry. if scrolling.is_none() { - self.viewport_coupled_widths.remove(&id); + record.coupled_width = None; } - let (geometry, had_geometry) = match self.window_geometry.entry(id) { - Entry::Occupied(entry) => (entry.into_mut(), true), - Entry::Vacant(entry) => (entry.insert(WindowGeometry::new(rect, client_size(&window))), false), - }; + + let natural_pending = record.natural_floating_pending && is_floating && !is_fullscreen && !is_maximized; + let had_geometry = record.geometry.is_some(); + let geometry = record + .geometry + .get_or_insert_with(|| WindowGeometry::new(rect, client_size(&window))); + let mode = if is_fullscreen { PresentationMode::Fullscreen } else if is_maximized { @@ -220,13 +243,16 @@ impl Ferese { if !was_mapped && (had_geometry || is_fullscreen) { geometry.settle_presentation(); } + if !self.animations_enabled { geometry.advance(Duration::ZERO, self.spring_config, false); requested_size = geometry.presentation_size_request(now).or(requested_size); } + if geometry.is_zooming() { - self.viewport_coupled_widths.remove(&id); + record.coupled_width = None; } + if let Some((workspace, world_x, viewport_x)) = scrolling { let restored_world_x = restored_scrolling_world_x(had_geometry, geometry.visual.current.x, viewport_x, world_x); @@ -239,23 +265,23 @@ impl Ferese { if !self.animations_enabled { animated_world_x.snap(); } + if !geometry.is_zooming() { geometry.visual.current.x = animated_world_x.current - viewport_x; geometry.visual.velocity.x = animated_world_x.velocity; } - scrolling_world_x.insert(id, (workspace, animated_world_x)); + record.world_x = Some((workspace, animated_world_x)); let coupled = !geometry.is_zooming() && (couple_width - || self - .viewport_coupled_widths - .get(&id) + || record + .coupled_width + .as_ref() .is_some_and(|(previous_workspace, _)| *previous_workspace == workspace)); if coupled { - let width = self - .viewport_coupled_widths - .entry(id) - .or_insert_with(|| (workspace, AnimatedValue::new(geometry.visual.current.width))); + let width = record + .coupled_width + .get_or_insert_with(|| (workspace, AnimatedValue::new(geometry.visual.current.width))); if width.0 != workspace { *width = (workspace, AnimatedValue::new(geometry.visual.current.width)); } @@ -266,15 +292,15 @@ impl Ferese { geometry.visual.current.width = width.1.current; geometry.visual.velocity.width = width.1.velocity; } else if pending_column_width_cycles.contains(&id) { - self.viewport_coupled_widths.remove(&id); + record.coupled_width = None; } } + let visual = geometry.visual.current; - let natural_pending = - self.natural_floating_pending.contains(&id) && is_floating && !is_fullscreen && !is_maximized; if natural_pending { requested_size = None; } + let location = (visual.x.round() as i32, visual.y.round() as i32); self.space.map_element(window.clone(), location, false); @@ -297,6 +323,7 @@ impl Ferese { } else { state.states.unset(xdg_toplevel::State::Maximized) }; + let tiled = !is_floating && !is_fullscreen; let tiled_changed = if tiled { state.states.set(xdg_toplevel::State::TiledLeft) @@ -319,7 +346,7 @@ impl Ferese { && had_geometry && self.animations_enabled { - self.resize_transactions.insert( + self.windows.set_transaction( id, crate::resize_transaction::ResizeTransaction::new(serial, now) .with_source_geometry(window.geometry()), @@ -327,10 +354,11 @@ impl Ferese { } } } - self.scrolling_world_x = scrolling_world_x; + if layout_changed { self.backdrop_generation = self.backdrop_generation.wrapping_add(1); } + self.sync_window_stacking(); self.retarget_overview(); self.send_shell_snapshots(); @@ -339,9 +367,10 @@ impl Ferese { } pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) { - if let Some(id) = self.window_ids.get(window) { + if let Some(id) = self.windows.ids().get(window) { self.window_stack.raise(*id); } + self.space.raise_element(window, activate); self.sync_window_stacking(); } @@ -349,8 +378,8 @@ impl Ferese { pub(crate) fn sync_window_stacking(&mut self) { let mut windows = self.space.elements().cloned().collect::>(); windows.sort_by_key(|window| { - let id = self.window_ids.get(window).copied(); - let geometry = id.and_then(|id| self.window_geometry.get(&id)); + 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( @@ -372,6 +401,7 @@ impl Ferese { if stacking_order_settled(self.space.elements(), &windows, |window| window.z_index()) { return; } + for window in windows { self.space.raise_element(&window, false); } @@ -387,7 +417,8 @@ impl Ferese { } pub(super) fn window_constraints(&self) -> HashMap { - self.window_ids + self.windows + .ids() .iter() .filter_map(|(window, id)| { let toplevel = window.toplevel()?; diff --git a/compositor/src/state/navigation.rs b/compositor/src/state/navigation.rs index a70efd6..e3dad4e 100644 --- a/compositor/src/state/navigation.rs +++ b/compositor/src/state/navigation.rs @@ -140,7 +140,8 @@ impl Ferese { tracing::warn!(%error, ?current, ?next, "failed to slide swipe focus"); } let Some(window) = self - .window_ids + .windows + .ids() .iter() .find_map(|(window, id)| (*id == next).then(|| window.clone())) else { @@ -277,7 +278,7 @@ impl Ferese { match self.workspaces.cycle_column_width(window, &self.column_width_presets) { Ok(true) => { - self.pending_column_width_cycles.insert(window); + self.windows.update(window, |w| w.column_width_pending = true); self.relayout(); } Ok(false) => {} @@ -306,7 +307,8 @@ impl Ferese { pub(crate) fn activate_managed_window(&mut self, id: WindowId) -> bool { let Some(window) = self - .window_ids + .windows + .ids() .iter() .find_map(|(window, window_id)| (*window_id == id).then(|| window.clone())) else { @@ -560,7 +562,7 @@ impl Ferese { return; } let surface = self.focused_window.and_then(|focused| { - self.window_ids.iter().find_map(|(window, id)| { + self.windows.ids().iter().find_map(|(window, id)| { (*id == focused) .then(|| window.toplevel()) .flatten() diff --git a/compositor/src/state/outputs.rs b/compositor/src/state/outputs.rs index 2174aad..fa7f069 100644 --- a/compositor/src/state/outputs.rs +++ b/compositor/src/state/outputs.rs @@ -48,7 +48,7 @@ impl Ferese { }; self.workspace_slides.remove(&output_id); - self.overview_scrims.remove(&output_id); + self.render.remove_output(output_id); self.pending_screencopies.retain(|capture| { if capture.output == *output { capture.fail(); @@ -82,7 +82,8 @@ impl Ferese { pub(crate) fn reposition_output_floats(&mut self, output: OutputId, old: Rect, new: Rect) { let floats = self - .window_ids + .windows + .ids() .values() .filter_map(|id| { let workspace = self.workspaces.workspace_for_window(*id)?; diff --git a/compositor/src/state/prediction.rs b/compositor/src/state/prediction.rs index 2772682..8de7f5b 100644 --- a/compositor/src/state/prediction.rs +++ b/compositor/src/state/prediction.rs @@ -1,49 +1,19 @@ use super::*; -/// Rendering samples an isolated forecast. Restore authoritative state before -/// returning to input, client commits, or another monitor's render deadline. -pub(crate) struct RenderForecast<'a> { - state: &'a mut Ferese, - geometry: Vec<(WindowId, WindowGeometry)>, - slides: HashMap, - closing: HashMap, - focus: HashMap, - dimming: HashMap, - overview: Option, +/// Output-local presentation values. Sampling never changes authoritative +/// geometry, configure barriers, animation clocks, or another output's view. +pub(crate) struct FrameScene { + pub windows: HashMap, + pub overview: crate::overview::OverviewMotion, } -impl std::ops::Deref for RenderForecast<'_> { - type Target = Ferese; - - fn deref(&self) -> &Ferese { - self.state - } -} - -impl std::ops::DerefMut for RenderForecast<'_> { - fn deref_mut(&mut self) -> &mut Ferese { - self.state - } -} - -impl Drop for RenderForecast<'_> { - fn drop(&mut self) { - if self.overview.is_none() { - return; - } - - for (id, geometry) in self.geometry.drain(..) { - self.state.window_geometry.insert(id, geometry); - } - - self.state.workspace_slides = std::mem::take(&mut self.slides); - self.state.closing_windows = std::mem::take(&mut self.closing); - self.state.window_focus = std::mem::take(&mut self.focus); - self.state.window_dimming = std::mem::take(&mut self.dimming); - if let Some(overview) = self.overview.take() { - self.state.overview.restore_prediction(overview); - } - } +pub(crate) struct WindowFrame { + pub geometry: WindowGeometry, + pub rect: Rect, + pub close_scale: f64, + pub close_alpha: f32, + pub focus: f64, + pub dim: f64, } impl Ferese { @@ -60,21 +30,24 @@ impl Ferese { return true; } - self.window_geometry.iter().any(|(id, geometry)| { + self.windows.records().any(|(id, record)| { + let Some(mut geometry) = record.geometry else { + return false; + }; + if !self.window_belongs_to_output(*id, output) { return false; } - if self.resize_transactions.contains_key(id) - || self.resize_snapshots.contains_key(id) - || self.closing_windows.get(id).is_some_and(|close| !close.close_sent) - || self.window_focus.get(id).is_some_and(|focus| focus.is_animating()) - || self.window_dimming.get(id).is_some_and(|dim| dim.is_animating()) + if record.resize.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()) { return true; } - let mut geometry = *geometry; if geometry.advance(Duration::ZERO, self.spring_config, self.animations_enabled) { return true; } @@ -90,104 +63,104 @@ impl Ferese { .as_ref() .is_some_and(|swipe| swipe.workspace == workspace); !held && viewport.advance(Duration::ZERO, self.viewport_spring_config) - }) || self.viewport_coupled_widths.get(id).is_some_and(|(_, width)| { + }) || record.coupled_width.as_ref().is_some_and(|(_, width)| { let mut width = *width; width.advance(Duration::ZERO, self.viewport_spring_config) }) }) } - pub(crate) fn forecast_render(&mut self, output: &Output, horizon: Duration) -> RenderForecast<'_> { - if horizon.is_zero() || !self.animations_enabled || !self.output_has_animations(output) { - return RenderForecast { - state: self, - geometry: Vec::new(), - slides: HashMap::new(), - closing: HashMap::new(), - focus: HashMap::new(), - dimming: HashMap::new(), - overview: None, - }; - } - - let delta = if self.animations_enabled { + pub(crate) fn sample_frame(&self, output: &Output, horizon: Duration) -> FrameScene { + let delta = if self.animations_enabled && self.output_has_animations(output) { horizon.min(Duration::from_millis(100)).mul_f64(self.animation_speed) } else { Duration::ZERO }; - let geometry = self - .window_geometry - .iter() - .filter(|(id, _)| self.window_belongs_to_output(**id, output)) - .map(|(id, geometry)| (*id, *geometry)) - .collect::>(); - let slides = self.workspace_slides.clone(); - let closing = self.closing_windows.clone(); - let focus = self.window_focus.clone(); - let dimming = self.window_dimming.clone(); - let overview = self.overview.predict(delta, self.spring_config); + let overview = self.overview.sample(delta, self.spring_config); let blocked = self - .resize_transactions - .keys() + .windows + .resizing() .filter_map(|id| self.workspaces.workspace_for_window(*id)) .collect::>(); - for (id, original) in &geometry { - let workspace = self.workspaces.workspace_for_window(*id); - if workspace.is_some_and(|workspace| blocked.contains(&workspace)) { + let mut windows = HashMap::new(); + + for (&id, record) in self.windows.records() { + let Some(original) = record.geometry else { continue }; + + if !self.window_belongs_to_output(id, output) { continue; } - let world = self.scrolling_world_x.get(id).and_then(|(workspace, world)| { + 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 .focus_swipe .as_ref() .is_some_and(|swipe| swipe.workspace == *workspace); + 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)) { + original + } else { + predict_geometry( + original, + delta, + self.spring_config, + self.viewport_spring_config, + world, + width, + ) + }; - let width = self.viewport_coupled_widths.get(id).map(|(_, width)| *width); - let predicted = predict_geometry( - *original, - delta, - self.spring_config, - self.viewport_spring_config, - world, - width, + let mut rect = overview.presented_rect(id, geometry.visual.current); + if !overview.is_presenting() { + let (x, y) = self.workspace_slide_offset_at(id, delta); + rect.x += x; + rect.y += y; + } + + let mut close = record.closing.as_ref().copied().unwrap_or_default(); + if record.closing.is_some() && !delta.is_zero() { + close.advance(delta, true); + } + + let eased = smoothstep(close.progress); + let focused = if self.overview.is_active() { + self.overview_selected(id) + } else { + self.focused_window == Some(id) + }; + + let sample_dim = |motion: Option<&DimAnimation>, fallback| { + let Some(mut motion) = motion.cloned() else { + return fallback; + }; + + if !delta.is_zero() { + motion.predict(delta, self.inactive_dim.duration_ms); + } + + motion.current + }; + + windows.insert( + id, + WindowFrame { + geometry, + rect, + close_scale: 1.0 - eased * 0.02, + close_alpha: (1.0 - eased) as f32, + focus: sample_dim(record.focus.as_ref(), if focused { 1.0 } else { 0.0 }), + dim: sample_dim(record.dimming.as_ref(), 0.0), + }, ); - self.window_geometry.insert(*id, predicted); } - for slide in self - .workspace_slides - .values_mut() - .filter(|slide| slide.held_progress.is_none()) - { - slide.advance(delta); - } - - for close in self.closing_windows.values_mut() { - close.advance(delta, self.animations_enabled); - } - - for focus in self.window_focus.values_mut() { - focus.predict(delta, self.inactive_dim.duration_ms); - } - - for dim in self.window_dimming.values_mut() { - dim.predict(delta, self.inactive_dim.duration_ms); - } - - RenderForecast { - state: self, - geometry, - slides, - closing, - focus, - dimming, - overview: Some(overview), - } + FrameScene { windows, overview } } } @@ -235,7 +208,7 @@ mod tests { #[test] #[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"] - fn render_forecast_restores_compositor_state_between_output_deadlines() { + fn frame_sampling_preserves_compositor_state_between_output_deadlines() { let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap()); assert!( runtime.starts_with(std::env::temp_dir()), @@ -269,7 +242,14 @@ mod tests { state.workspaces.insert_window(id, Axis::Horizontal, 0.5).unwrap(); let mut geometry = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None); geometry.visual.set_target(Rect::new(600., 0., 400., 300.)); - state.window_geometry.insert(id, geometry); + // This test isolates frame sampling from the protocol client fixture. + state.windows.records.insert( + id, + super::super::window_registry::WindowRecord { + geometry: Some(geometry), + ..Default::default() + }, + ); let tick = state.last_animation_tick; for horizon in [ Duration::from_millis(16), @@ -277,17 +257,27 @@ mod tests { Duration::from_millis(8), ] { { - let forecast = state.forecast_render(&output, horizon); - assert!(forecast.window_geometry[&id].visual.current.x > geometry.visual.current.x); + let forecast = state.sample_frame(&output, horizon); + assert!(forecast.windows[&id].geometry.visual.current.x > geometry.visual.current.x); } - assert_eq!(state.window_geometry[&id], geometry); + assert_eq!(*state.windows.geometry(&id).unwrap(), geometry); assert_eq!(state.last_animation_tick, tick); } + // A prediction must neither advance nor expire a client resize barrier. + state.windows.set_transaction( + id, + crate::resize_transaction::ResizeTransaction::new(9.into(), Duration::ZERO), + ); + let blocked = state.sample_frame(&output, Duration::from_millis(16)); + assert_eq!(blocked.windows[&id].geometry, geometry); + assert!(state.windows.transaction(&id).is_some()); + state.windows.clear_transaction(&id); + state.animations_enabled = false; - let forecast = state.forecast_render(&output, Duration::from_millis(16)); - assert_eq!(forecast.window_geometry[&id], geometry); + let forecast = state.sample_frame(&output, Duration::from_millis(16)); + assert_eq!(forecast.windows[&id].geometry, geometry); } #[test] diff --git a/compositor/src/state/window_registry.rs b/compositor/src/state/window_registry.rs new file mode 100644 index 0000000..21afc47 --- /dev/null +++ b/compositor/src/state/window_registry.rs @@ -0,0 +1,253 @@ +use std::collections::{HashMap, HashSet}; +use std::hash::Hash; + +use ferese_animation::{AnimatedValue, WindowGeometry}; +use ferese_core::WorkspaceId; +use ferese_layout::{ColumnWidth, Rect, WindowId}; + +use crate::resize_transaction::ResizeTransaction; + +/// Metadata with exactly the lifetime of a managed window. Layouts and focus +/// history reference its ID; they do not own or duplicate this metadata. +#[derive(Default)] +pub(crate) struct WindowRecord { + pub geometry: Option, + pub resize: Option, + pub focus: Option, + pub dimming: Option, + pub(super) closing: Option, + pub world_x: Option<(WorkspaceId, AnimatedValue)>, + pub coupled_width: Option<(WorkspaceId, AnimatedValue)>, + pub maximized: bool, + pub maximized_column_width: Option, + pub natural_floating_pending: bool, + pub floating_memory: Option, + pub placement_anchor: Option, + pub resize_anchor: Option<(bool, bool, Rect)>, + pub column_width_pending: bool, + pub rules_applied: bool, +} + +/// The handle index is read-only outside this owner. Registering and removing a +/// handle always updates its record as well. IDs are never reused on remapping. +pub(crate) struct WindowRegistry { + ids: HashMap, + pub(super) records: HashMap, + next_id: u64, +} + +impl Default for WindowRegistry { + fn default() -> Self { + Self { + ids: HashMap::new(), + records: HashMap::new(), + next_id: 1, + } + } +} + +impl WindowRegistry { + pub fn allocate_id(&mut self) -> WindowId { + let id = WindowId(self.next_id); + self.next_id = self.next_id.checked_add(1).expect("window IDs exhausted"); + + id + } + + pub fn register(&mut self, window: W, id: WindowId) { + assert!(!self.ids.contains_key(&window), "window already registered"); + assert!(!self.records.contains_key(&id), "window ID already registered"); + + self.ids.insert(window, id); + self.records.insert(id, WindowRecord::default()); + } + + pub fn ids(&self) -> &HashMap { + &self.ids + } + + pub fn record(&self, id: WindowId) -> Option<&WindowRecord> { + self.records.get(&id) + } + + pub fn record_mut(&mut self, id: WindowId) -> Option<&mut WindowRecord> { + 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 take_world_positions(&mut self) -> HashMap { + self.records + .iter_mut() + .filter_map(|(id, record)| Some((*id, record.world_x.take()?))) + .collect() + } + + pub fn geometry(&self, id: &WindowId) -> Option<&WindowGeometry> { + self.records.get(id)?.geometry.as_ref() + } + + pub fn geometry_mut(&mut self, id: &WindowId) -> Option<&mut WindowGeometry> { + self.records.get_mut(id)?.geometry.as_mut() + } + + pub fn geometries(&self) -> impl Iterator { + self.records + .iter() + .filter_map(|(id, record)| Some((id, record.geometry.as_ref()?))) + } + + pub fn set_geometry(&mut self, id: WindowId, geometry: WindowGeometry) { + self.update(id, |record| record.geometry = Some(geometry)); + } + + pub fn transaction(&self, id: &WindowId) -> Option<&ResizeTransaction> { + self.records.get(id)?.resize.as_ref() + } + + pub fn set_transaction(&mut self, id: WindowId, transaction: ResizeTransaction) { + self.update(id, |record| record.resize = Some(transaction)); + } + + pub fn clear_transaction(&mut self, id: &WindowId) { + self.update(*id, |record| record.resize = None); + } + + pub fn resizing(&self) -> impl Iterator { + self.records + .iter() + .filter_map(|(id, record)| record.resize.is_some().then_some(id)) + } + + pub fn expire_transactions(&mut self, now: std::time::Duration) { + for (id, record) in &mut self.records { + if record.resize.is_some_and(|transaction| transaction.expired(now)) { + tracing::warn!(?id, "resize presentation deadline reached"); + record.resize = None; + } + } + } + + pub fn update(&mut self, id: WindowId, update: impl FnOnce(&mut WindowRecord) -> R) -> Option { + self.records.get_mut(&id).map(update) + } + + pub fn remove(&mut self, window: &W) -> Option { + let id = self.ids.remove(window)?; + self.records.remove(&id); + + Some(id) + } + + pub fn take_column_width_requests(&mut self) -> HashSet { + self.records + .iter_mut() + .filter_map(|(id, record)| std::mem::take(&mut record.column_width_pending).then_some(*id)) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unmap_drops_metadata_and_late_updates_cannot_resurrect_it() { + let mut windows = WindowRegistry::default(); + let id = windows.allocate_id(); + windows.register("surface", id); + windows.update(id, |record| { + record.maximized = true; + record.resize_anchor = Some((true, false, Rect::new(0., 0., 800., 600.))); + record.column_width_pending = true; + record.rules_applied = true; + record.focus = Some(crate::dimming::DimAnimation::new(1.0)); + record.closing = Some(super::super::ClosingAnimation::default()); + record.world_x = Some((WorkspaceId(1), AnimatedValue::new(50.0))); + record.coupled_width = Some((WorkspaceId(1), AnimatedValue::new(800.0))); + }); + let geometry = WindowGeometry::new(Rect::new(0., 0., 800., 600.), None); + let resize = ResizeTransaction::new(12.into(), std::time::Duration::ZERO); + windows.set_geometry(id, geometry); + windows.set_transaction(id, resize); + + assert_eq!(windows.remove(&"surface"), Some(id)); + assert_eq!(windows.remove(&"surface"), None); + assert!(windows.record(id).is_none()); + assert!(windows.update(id, |record| record.maximized = true).is_none()); + + windows.set_geometry(id, geometry); + windows.set_transaction(id, resize); + + assert!(windows.geometry(&id).is_none()); + assert!(windows.transaction(&id).is_none()); + assert_eq!(windows.resizing().count(), 0); + assert!(windows.ids().is_empty()); + assert!(windows.take_column_width_requests().is_empty()); + } + + #[test] + fn remap_has_new_identity_and_does_not_inherit_old_requests() { + let mut windows = WindowRegistry::default(); + let old = windows.allocate_id(); + windows.register("surface", old); + windows.update(old, |record| record.rules_applied = true); + windows.remove(&"surface"); + + let new = windows.allocate_id(); + windows.register("surface", new); + assert_ne!(old, new); + assert!( + windows + .update(old, |record| record.column_width_pending = true) + .is_none() + ); + assert!(!windows.record(new).unwrap().rules_applied); + assert!(windows.take_column_width_requests().is_empty()); + } + + #[test] + fn pending_width_changes_are_consumed_once_and_survive_other_window_removal() { + let mut windows = WindowRegistry::default(); + let first = windows.allocate_id(); + let second = windows.allocate_id(); + windows.register("first", first); + windows.register("second", second); + windows.update(first, |record| record.column_width_pending = true); + windows.update(second, |record| record.column_width_pending = true); + + windows.remove(&"first"); + + assert_eq!(windows.take_column_width_requests(), HashSet::from([second])); + assert!(windows.take_column_width_requests().is_empty()); + assert!(windows.record(second).is_some()); + } + + #[test] + fn expired_resize_releases_only_its_barrier_and_preserves_geometry() { + use std::time::Duration; + + let mut windows = WindowRegistry::default(); + let first = windows.allocate_id(); + let second = windows.allocate_id(); + windows.register("first", first); + windows.register("second", second); + let geometry = WindowGeometry::new(Rect::new(0., 0., 800., 600.), None); + windows.set_geometry(first, geometry); + windows.set_transaction(first, ResizeTransaction::new(1.into(), Duration::ZERO)); + windows.set_transaction(second, ResizeTransaction::new(2.into(), Duration::from_millis(100))); + + windows.expire_transactions(Duration::from_millis(300)); + + assert_eq!(windows.geometry(&first), Some(&geometry)); + assert!(windows.transaction(&first).is_none()); + assert!(windows.transaction(&second).unwrap().accepts(Some(2.into()))); + assert_eq!(windows.resizing().copied().collect::>(), vec![second]); + } +} diff --git a/compositor/src/state/windows.rs b/compositor/src/state/windows.rs index c4f6f52..ebf1bd3 100644 --- a/compositor/src/state/windows.rs +++ b/compositor/src/state/windows.rs @@ -24,6 +24,7 @@ impl Ferese { { return; } + let outputs = self.floating_outputs(); let Some((remembered, work)) = app_id .as_deref() @@ -65,13 +66,16 @@ impl Ferese { self.add_tiled_window(window); return; } + if let Some(parent) = parent { self.add_transient_window(window, parent); return; } + if let Some(pointer) = self.seat.get_pointer() { self.focus_output_at(pointer.current_location()); } + let remembered_work = app_id .filter(|app| !crate::window_rules::is_native_dialog(Some(app))) .and_then(|app| self.floating_memory.get(app)) @@ -86,12 +90,13 @@ impl Ferese { if let Some(work) = remembered_work { self.focus_output_at((work.x + work.width / 2.0, work.y + work.height / 2.0).into()); } + let Some(bounds) = self.output_bounds() else { self.add_tiled_window(window); return; }; - let id = WindowId(self.next_window_id); - self.next_window_id += 1; + + let id = self.windows.allocate_id(); let size_rect = client_size(&window) .map(|size| natural_floating_rect(bounds, size)) .unwrap_or_else(|| centered_floating_rect(bounds)); @@ -100,7 +105,6 @@ impl Ferese { .focused_window .and_then(|focused| self.presented_window_rect(focused)) .unwrap_or(bounds); - self.floating_placement_anchors.insert(id, anchor); let fullscreen = self.workspaces.active().fullscreen; let focus = true; if let Err(error) = self @@ -110,25 +114,30 @@ impl Ferese { tracing::error!(%error, ?id, "failed to insert rule-placed floating window"); return; } + + self.windows.register(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() { - self.natural_floating_pending.insert(id); + self.windows.update(id, |w| w.natural_floating_pending = true); } - self.window_ids.insert(window.clone(), id); + self.window_stack.insert(id); if let Some(fullscreen) = fullscreen { self.floating_above_fullscreen.insert(id, fullscreen); } + if focus { self.focused_window = Some(id); } + self.space.map_element(window, (0, 0), focus); // apply_initial_window_rules performs the first relayout/configure. } pub fn add_tiled_window(&mut self, window: Window) { - let id = WindowId(self.next_window_id); - self.next_window_id += 1; + let id = self.windows.allocate_id(); let focus_new_window = self.workspaces.active().fullscreen.is_none(); let axis = self @@ -147,11 +156,12 @@ impl Ferese { return; } - self.window_ids.insert(window.clone(), id); + self.windows.register(window.clone(), id); self.window_stack.insert(id); if focus_new_window { self.focused_window = Some(id); } + self.space.map_element(window, (0, 0), focus_new_window); self.relayout(); @@ -165,18 +175,19 @@ impl Ferese { self.add_tiled_window(window); return; }; + let Some(bounds) = self.floating_bounds_for_window(parent) else { self.add_tiled_window(window); return; }; + let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds); let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds); let size_rect = client_size(&window) .map(|s| natural_floating_rect(bounds, s)) .unwrap_or_else(|| centered_transient_rect(visible_parent)); let rect = self.place_floating(&window, None, bounds, (size_rect.width, size_rect.height), None); - let id = WindowId(self.next_window_id); - self.next_window_id += 1; + let id = self.windows.allocate_id(); let focus = workspace == self.workspaces.active_id() && (self.focused_window == Some(parent) || self.workspaces.active().fullscreen == Some(parent)); @@ -185,11 +196,12 @@ impl Ferese { return; } - self.window_ids.insert(window.clone(), id); + self.windows.register(window.clone(), id); self.window_stack.insert(id); if client_size(&window).is_none() { - self.natural_floating_pending.insert(id); + self.windows.update(id, |w| w.natural_floating_pending = true); } + if self .workspaces .workspace(workspace) @@ -197,9 +209,11 @@ impl Ferese { { self.floating_above_fullscreen.insert(id, parent); } + if focus { self.focused_window = Some(id); } + self.space.map_element(window, (0, 0), focus); self.relayout(); if focus { @@ -214,10 +228,15 @@ impl Ferese { title: Option<&str>, transient: bool, ) { - let Some(id) = self.window_ids.get(window).copied() else { + let Some(id) = self.windows.ids().get(window).copied() else { return; }; - if !self.window_rules_applied.insert(id) { + + if !self + .windows + .update(id, |w| !std::mem::replace(&mut w.rules_applied, true)) + .unwrap_or(false) + { return; } @@ -226,11 +245,12 @@ impl Ferese { } pub(super) fn reapply_window_rules(&mut self, old_rules: &[WindowRule]) { - let windows = self.window_ids.keys().cloned().collect::>(); + let windows = self.windows.ids().keys().cloned().collect::>(); for window in windows { let Some(toplevel) = window.toplevel() else { continue; }; + let (app_id, title, transient) = with_states(toplevel.wl_surface(), |states| { let attributes = states.data_map.get::().unwrap().lock().unwrap(); ( @@ -248,15 +268,18 @@ impl Ferese { } pub(super) fn apply_window_rule_result(&mut self, window: &Window, rule: crate::window_rules::WindowRuleResult) { - let Some(id) = self.window_ids.get(window).copied() else { + let Some(id) = self.windows.ids().get(window).copied() else { return; }; + let Some(bounds) = self.floating_bounds_for_window(id) else { return; }; + if rule == Default::default() { return; } + let axis = self .workspaces .active() @@ -269,6 +292,7 @@ impl Ferese { { tracing::warn!(%error, ?id, workspace, "failed to apply window workspace rule"); } + let bounds = self.floating_bounds_for_window(id).unwrap_or(bounds); let should_float = rule @@ -287,7 +311,7 @@ impl Ferese { .and_then(|entry| entry.restore(&self.floating_outputs())) .is_none() { - self.natural_floating_pending.insert(id); + self.windows.update(id, |w| w.natural_floating_pending = true); } } rect.width = rule.width.unwrap_or(rect.width).min(bounds.width); @@ -309,6 +333,7 @@ impl Ferese { } else { Ok(()) }; + if let Err(error) = result { tracing::warn!(%error, ?id, "failed to apply floating window rule"); } @@ -331,16 +356,19 @@ impl Ferese { } pub fn make_window_transient(&mut self, window: &Window, parent: WindowId) { - let Some(id) = self.window_ids.get(window).copied() else { + let Some(id) = self.windows.ids().get(window).copied() else { return; }; + if self.workspaces.workspace_for_window(id) != self.workspaces.workspace_for_window(parent) { tracing::warn!(?id, ?parent, "ignored transient parent on another workspace"); return; } + let Some(bounds) = self.floating_bounds_for_window(parent) else { return; }; + let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds); let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds); let size_rect = client_size(window) @@ -369,6 +397,7 @@ impl Ferese { { self.floating_above_fullscreen.insert(id, parent); } + if self.focused_window == Some(parent) { if let Err(error) = self.workspaces.focus_window(id) { tracing::warn!(%error, ?id, "failed to focus transient window"); @@ -385,35 +414,18 @@ impl Ferese { pub fn remove_tiled_window(&mut self, window: &Window) { // Uncommitted toplevels have not entered a workspace yet. self.space.unmap_elem(window); - let Some(id) = self.window_ids.remove(window) else { + let Some(id) = self.windows.remove(window) else { return; }; + self.focus_history.remove(id); - self.window_geometry.remove(&id); - self.resize_transactions.remove(&id); - self.resize_snapshots.remove(&id); - self.window_resize_fills.remove(&id); - self.maximized_windows.remove(&id); - self.maximized_column_widths.remove(&id); + self.render.remove_window(id); self.window_stack.remove(id); self.floating_above_fullscreen.remove(&id); - self.natural_floating_pending.remove(&id); - self.floating_window_memory.remove(&id); - self.floating_placement_anchors.remove(&id); - self.floating_resize_anchors.remove(&id); - self.window_borders.remove(&id); - self.window_dims.remove(&id); - self.window_dimming.remove(&id); - self.window_focus.remove(&id); - self.window_shadows.remove(&id); - self.closing_windows.remove(&id); - self.window_rules_applied.remove(&id); - self.scrolling_world_x.remove(&id); - self.viewport_coupled_widths.remove(&id); - self.pending_column_width_cycles.remove(&id); if let Err(error) = self.workspaces.remove_window(id) { tracing::error!(%error, ?id, "failed to remove window from layout"); } + if self.focused_window == Some(id) { self.focused_window = self.workspaces.active().last_focused; } @@ -423,7 +435,8 @@ impl Ferese { pub(crate) fn capture_resize_before_commit(&mut self, surface: &WlSurface) { let Some((window, id)) = self - .window_ids + .windows + .ids() .iter() .find(|(window, _)| { window @@ -434,9 +447,11 @@ impl Ferese { else { return; }; - let Some(transaction) = self.resize_transactions.get(&id) else { + + let Some(transaction) = self.windows.transaction(&id) else { return; }; + let serial = with_states(surface, |states| { states .data_map @@ -446,6 +461,7 @@ impl Ferese { if !transaction.accepts(serial) { return; } + let Some(output) = self .space .outputs() @@ -454,10 +470,11 @@ impl Ferese { else { return; }; + let source_geometry = transaction.source_geometry().unwrap_or_else(|| window.geometry()); let scale = output.current_scale().fractional_scale(); let source_size = source_geometry.size.to_physical_precise_round(scale); - if self.resize_snapshots.get(&id).is_some_and(|snapshot| { + if self.render.snapshot(&id).is_some_and(|snapshot| { crate::presentation::snapshot_covers_source( smithay::backend::renderer::Texture::size(&snapshot.texture), source_size, @@ -467,7 +484,8 @@ impl Ferese { }) { return; } - let used: usize = self.resize_snapshots.values().map(|snapshot| snapshot.bytes()).sum(); + + let used: usize = self.render.snapshots().map(|snapshot| snapshot.bytes()).sum(); let remaining = crate::presentation::SNAPSHOT_BUDGET.saturating_sub(used); let result = if let Some(backend) = &self.nested_backend { // Commit dispatch does not run inside the Winit event callback; @@ -490,7 +508,7 @@ impl Ferese { match result { Ok(Some(snapshot)) => { tracing::debug!(?id, bytes = snapshot.bytes(), "captured native resize handoff"); - self.resize_snapshots.insert(id, snapshot); + self.render.set_snapshot(id, snapshot); } Ok(None) => {} Err(error) => { @@ -500,7 +518,7 @@ impl Ferese { } pub fn record_client_commit(&mut self, window: &Window) { - let Some(&id) = self.window_ids.get(window) else { + let Some(&id) = self.windows.ids().get(window) else { return; }; // current_serial is promoted by Smithay on commit, unlike @@ -514,28 +532,30 @@ impl Ferese { }) }); if self - .resize_transactions - .get(&id) + .windows + .transaction(&id) .is_some_and(|transaction| transaction.accepts(committed_serial)) { - self.resize_transactions.remove(&id); + self.windows.clear_transaction(&id); } + let Some(size) = client_size(window) else { return; }; + let fullscreen = self .workspaces .workspace_for_window(id) .and_then(|workspace| self.workspaces.workspace(workspace)) .is_some_and(|workspace| workspace.fullscreen == Some(id)); let normal_geometry = !fullscreen - && !self.maximized_windows.contains(&id) - && self - .window_geometry - .get(&id) - .is_none_or(|geometry| !geometry.is_zooming()); - let initial_floating = - self.natural_floating_pending.remove(&id) || self.floating_placement_anchors.contains_key(&id); + && !self.windows.record(id).is_some_and(|w| w.maximized) + && self.windows.geometry(&id).is_none_or(|geometry| !geometry.is_zooming()); + let initial_floating = self + .windows + .update(id, |w| std::mem::take(&mut w.natural_floating_pending)) + .unwrap_or(false) + || self.windows.record(id).is_some_and(|w| w.placement_anchor.is_some()); if initial_floating && normal_geometry && matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. })) @@ -548,12 +568,12 @@ impl Ferese { (f64::from(size.width), f64::from(size.height)), Some((Some(f64::from(size.width)), Some(f64::from(size.height)))), ); - self.floating_placement_anchors.remove(&id); + self.windows.update(id, |w| w.placement_anchor = None); if self.workspaces.set_floating_rect(id, rect).is_ok() { // This is the first mapped client buffer, not a user resize. // Do not animate from the temporary placement box. - self.window_geometry.insert(id, WindowGeometry::new(rect, Some(size))); - self.resize_transactions.remove(&id); + self.windows.set_geometry(id, WindowGeometry::new(rect, Some(size))); + self.windows.clear_transaction(&id); self.relayout(); } } @@ -561,7 +581,7 @@ impl Ferese { // terminal cell grids, or a dialog changing its contents). Its committed // window geometry is authoritative once the latest configure is committed. // Never let an old buffer undo a newer resize or a fullscreen transition. - self.floating_placement_anchors.remove(&id); + self.windows.update(id, |w| w.placement_anchor = None); let settled_configure = window.toplevel().is_some_and(|toplevel| { with_states(toplevel.wl_surface(), |states| { states.data_map.get::().is_some_and(|data| { @@ -581,22 +601,25 @@ impl Ferese { && ClientSize::from_rect(rect) != size { let mut rect = Rect::new(rect.x, rect.y, f64::from(size.width), f64::from(size.height)); - if let Some((left, top, anchor)) = self.floating_resize_anchors.get(&id) { + if let Some((left, top, anchor)) = self.windows.record(id).and_then(|w| w.resize_anchor.as_ref()) { rect = crate::floating::anchored_size(*anchor, (rect.width, rect.height), *left, *top); } else if initial_floating && let Some(work) = self.floating_bounds_for_window(id) { rect = crate::floating::clamp_to_work(rect, work); } + if self.workspaces.set_floating_rect(id, rect).is_ok() { - self.window_geometry.insert(id, WindowGeometry::new(rect, Some(size))); - self.resize_transactions.remove(&id); - self.resize_snapshots.remove(&id); + self.windows.set_geometry(id, WindowGeometry::new(rect, Some(size))); + self.windows.clear_transaction(&id); + self.render.clear_snapshot(&id); self.relayout(); } } - if settled_configure && self.floating_resize_anchors.remove(&id).is_some() { + + if settled_configure && self.windows.update(id, |w| w.resize_anchor.take()).flatten().is_some() { self.remember_floating(window); } - let Some(geometry) = self.window_geometry.get_mut(&id) else { + + let Some(geometry) = self.windows.geometry_mut(&id) else { return; }; @@ -614,17 +637,21 @@ impl Ferese { return; } - self.closing_windows.entry(focused).or_default(); + self.windows.update(focused, |record| { + record.closing.get_or_insert_default(); + }); crate::backends::direct::render_all(self); } pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool { - if !self.window_ids.values().any(|window_id| *window_id == id) { + if !self.windows.ids().values().any(|window_id| *window_id == id) { return false; } if self.animations_enabled { - self.closing_windows.entry(id).or_default(); + self.windows.update(id, |record| { + record.closing.get_or_insert_default(); + }); crate::backends::direct::render_all(self); } else { self.send_window_close(id); @@ -635,7 +662,8 @@ impl Ferese { pub(super) fn send_window_close(&self, id: WindowId) { let Some(toplevel) = self - .window_ids + .windows + .ids() .iter() .find_map(|(window, window_id)| (*window_id == id).then(|| window.toplevel()).flatten()) else { diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 9fa4228..d8a4ac1 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -229,7 +229,8 @@ fn send_nested_frame_callbacks(state: &mut Ferese, output: &Output, refresh: Dur let time = state.start_time.elapsed(); for window in state.space.elements().filter(|window| { state - .window_ids + .windows + .ids() .get(*window) .is_some_and(|id| state.window_belongs_to_output(*id, output)) }) { diff --git a/scripts/tests/test_unmapped_toplevel_isolated.py b/scripts/tests/test_unmapped_toplevel_isolated.py index eccae4b..c7b91d3 100644 --- a/scripts/tests/test_unmapped_toplevel_isolated.py +++ b/scripts/tests/test_unmapped_toplevel_isolated.py @@ -86,6 +86,7 @@ class UnmappedToplevelTest(unittest.TestCase): text=True, ) + visible_ids = [] for phase, visible in [ ("initial-empty", False), ("visible", True), @@ -110,9 +111,35 @@ class UnmappedToplevelTest(unittest.TestCase): if time.monotonic() >= deadline: self.fail(f"{phase}: unexpected focused window {focused}") time.sleep(0.05) + if visible: + visible_ids.append(focused["id"]) + if phase == "visible": + # Leave a configure/resize outstanding when the buffer is detached. + subprocess.run([str(ctl_binary), "toggle-maximized"], env=env, + capture_output=True, check=True) + client.stdin.write("\n") client.stdin.flush() + self.assertNotEqual(visible_ids[0], visible_ids[1], "remapping must allocate a fresh window identity") self.assertEqual(client.wait(timeout=5), 0) + + deadline = time.monotonic() + 3 + while True: + remaining = json.loads(subprocess.check_output( + [str(ctl_binary), "get-windows"], env=env, text=True + )) + if not remaining: + break + if time.monotonic() >= deadline: + self.fail(f"destroyed window still managed: {remaining}") + time.sleep(0.05) + + subprocess.run([str(ctl_binary), "focus-last-window"], env=env, + capture_output=True, check=True) + focused = json.loads(subprocess.check_output( + [str(ctl_binary), "get-focused-window"], env=env, text=True + )) + self.assertIsNone(focused, "focus history must not restore a destroyed window") finally: if client and client.poll() is None: client.terminate()