From 3384cfde0f1af953e4b7aaebaa39b0e56a3691ae Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Fri, 25 Sep 2026 06:44:07 +0100 Subject: [PATCH] feat: add compositor overview scene --- README.md | 5 + compositor/src/config.rs | 17 +- compositor/src/input.rs | 17 +- compositor/src/main.rs | 1 + compositor/src/overview.rs | 408 ++++++++++++++++++++++++++++++++ compositor/src/shell_control.rs | 14 +- compositor/src/state.rs | 48 +++- compositor/src/winit.rs | 55 ++++- 8 files changed, 539 insertions(+), 26 deletions(-) create mode 100644 compositor/src/overview.rs diff --git a/README.md b/README.md index 099dcf4..4390f23 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,11 @@ to center the focused column, `Super+[` to consume the focused window into an adjacent column, and `Super+]` to expel it. `Super+Ctrl+H/J/K/L` resizes in the corresponding direction. +`Super+Tab` toggles the compositor overview. Its live window previews retain +their client surfaces, `Super+H/J/K/L` moves the selection using the presented +preview geometry, and clicking a preview activates it without forwarding the +selection click to the application. + Keyboard and direct-session touchpad settings are applied at startup: ```toml diff --git a/compositor/src/config.rs b/compositor/src/config.rs index 0651b89..38d2825 100644 --- a/compositor/src/config.rs +++ b/compositor/src/config.rs @@ -247,6 +247,7 @@ pub enum BindingAction { Consume, Expel, ToggleFloating, + ToggleOverview, } impl Binding { @@ -1097,6 +1098,10 @@ fn parse_action( no_argument()?; Ok(BindingAction::ToggleFloating) } + "toggle-overview" => { + no_argument()?; + Ok(BindingAction::ToggleOverview) + } _ => Err(ConfigError::InvalidBinding(format!( "unknown action {action:?}" ))), @@ -1135,6 +1140,7 @@ fn default_bindings() -> Vec { binding("Super+[", "consume", None), binding("Super+]", "expel", None), binding("Super+Shift+Space", "toggle-floating", None), + binding("Super+Tab", "toggle-overview", None), ]; for (key, direction) in [("H", "left"), ("J", "down"), ("K", "up"), ("L", "right")] { @@ -1622,12 +1628,17 @@ mod tests { let input = config.input_settings().unwrap(); let bindings = config.bindings(&input).unwrap(); - assert_eq!(bindings.len(), 39); + assert_eq!(bindings.len(), 40); assert!(bindings.iter().any(|binding| { binding.modifiers.logo && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Return) && binding.action == BindingAction::Spawn(vec!["foot".to_owned()]) })); + assert!(bindings.iter().any(|binding| { + binding.modifiers.logo + && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Tab) + && binding.action == BindingAction::ToggleOverview + })); } #[test] @@ -1639,7 +1650,7 @@ mod tests { let input = replaced.input_settings().unwrap(); let bindings = replaced.bindings(&input).unwrap(); - assert_eq!(bindings.len(), 39); + assert_eq!(bindings.len(), 40); assert!(bindings.iter().any(|binding| { binding.action == BindingAction::Spawn(vec![ @@ -1651,7 +1662,7 @@ mod tests { let input = unbound.input_settings().unwrap(); let bindings = unbound.bindings(&input).unwrap(); - assert_eq!(bindings.len(), 38); + assert_eq!(bindings.len(), 39); assert!( !bindings .iter() diff --git a/compositor/src/input.rs b/compositor/src/input.rs index f481ce4..fc937fb 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -167,6 +167,17 @@ impl Ferese { let pointer = self.seat.get_pointer().expect("seat has a pointer"); let serial = SERIAL_COUNTER.next_serial(); + if self.overview.is_active() { + if event.state() == ButtonState::Pressed + && let Some(window) = self.window_under_visual(pointer.current_location()) + && let Some(id) = self.window_ids.get(&window).copied() + { + self.select_overview_window(id); + } + + return; + } + if event.state() == ButtonState::Pressed && !pointer.is_grabbed() { self.focus_window_at(pointer.current_location(), serial); } @@ -334,7 +345,10 @@ impl Ferese { pointer: &PointerHandle, position: Point, ) { - if !self.input_settings.focus_follows_mouse || pointer.is_grabbed() { + if self.overview.is_active() + || !self.input_settings.focus_follows_mouse + || pointer.is_grabbed() + { return; } @@ -467,6 +481,7 @@ impl Ferese { BindingAction::Consume => self.consume_focused_window(), BindingAction::Expel => self.expel_focused_window(), BindingAction::ToggleFloating => self.toggle_focused_floating(), + BindingAction::ToggleOverview => self.toggle_overview(), } } } diff --git a/compositor/src/main.rs b/compositor/src/main.rs index 7ff1737..300d3f4 100644 --- a/compositor/src/main.rs +++ b/compositor/src/main.rs @@ -9,6 +9,7 @@ mod handlers; mod input; mod ipc; mod metrics; +mod overview; mod private_client; mod shell_control; mod stacking; diff --git a/compositor/src/overview.rs b/compositor/src/overview.rs new file mode 100644 index 0000000..da10b66 --- /dev/null +++ b/compositor/src/overview.rs @@ -0,0 +1,408 @@ +use std::{collections::HashMap, time::Duration}; + +use ferese_animation::{AnimatedRect, SpringConfig}; +use ferese_layout::{Direction, Rect, WindowId}; + +use crate::Ferese; + +const OVERVIEW_MARGIN: f64 = 48.0; +const OVERVIEW_GAP: f64 = 24.0; +const MAX_PREVIEW_SCALE: f64 = 0.82; + +#[derive(Debug, Default)] +pub(crate) struct OverviewState { + active: bool, + selected: Option, + presentations: HashMap, +} + +impl OverviewState { + pub(crate) fn is_active(&self) -> bool { + self.active + } + + pub(crate) fn is_presenting(&self) -> bool { + self.active || !self.presentations.is_empty() + } + + pub(crate) fn selected(&self) -> Option { + self.selected + } + + pub(crate) fn select_direction(&mut self, direction: Direction) -> bool { + let Some(current) = self + .selected + .and_then(|id| self.presentations.get(&id)) + .map(|presentation| presentation.current) + else { + return false; + }; + let current_center = rect_center(current); + let next = self + .presentations + .iter() + .filter(|(id, _)| Some(**id) != self.selected) + .filter_map(|(id, presentation)| { + let center = rect_center(presentation.current); + directional_distance(direction, current_center, center) + .map(|distance| (*id, distance)) + }) + .min_by(|(left_id, left), (right_id, right)| { + left.total_cmp(right) + .then_with(|| left_id.0.cmp(&right_id.0)) + }) + .map(|(id, _)| id); + let Some(next) = next else { + return false; + }; + + self.selected = Some(next); + true + } + + pub(crate) fn presented_rect(&self, id: WindowId, normal: Rect) -> Rect { + self.presentations + .get(&id) + .map_or(normal, |presentation| presentation.current) + } + + fn enter( + &mut self, + targets: HashMap, + selected: Option, + animations_enabled: bool, + ) { + self.active = true; + self.selected = selected + .filter(|id| targets.contains_key(id)) + .or_else(|| targets.keys().copied().min_by_key(|id| id.0)); + self.retarget(targets, animations_enabled); + } + + fn retarget(&mut self, targets: HashMap, animations_enabled: bool) { + self.presentations.retain(|id, _| targets.contains_key(id)); + + for (id, (normal, target)) in targets { + let presentation = self + .presentations + .entry(id) + .or_insert_with(|| AnimatedRect::new(normal)); + + presentation.set_target(target); + if !animations_enabled { + presentation.snap(); + } + } + } + + fn exit(&mut self, normal: HashMap, animations_enabled: bool) { + self.active = false; + self.selected = None; + self.presentations.retain(|id, presentation| { + let Some(target) = normal.get(id).copied() else { + return false; + }; + + presentation.set_target(target); + if !animations_enabled { + presentation.snap(); + } + true + }); + + if !animations_enabled { + self.presentations.clear(); + } + } + + pub(crate) fn advance( + &mut self, + delta: Duration, + spring: SpringConfig, + animations_enabled: bool, + ) -> bool { + 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(); + } + + active_animation + } +} + +impl Ferese { + pub(crate) fn set_overview_active(&mut self, active: bool) { + if self.overview.is_active() == active { + return; + } + + if active { + let targets = self.overview_targets(); + self.overview + .enter(targets, self.focused_window, self.animations_enabled()); + } else { + let normal = self.normal_window_rects(); + self.overview.exit(normal, self.animations_enabled()); + } + + self.notify_overview_state(); + crate::backends::direct::render_all(self); + } + + pub(crate) fn toggle_overview(&mut self) { + self.set_overview_active(!self.overview.is_active()); + } + + pub(crate) fn focus_overview_direction(&mut self, direction: Direction) -> bool { + if !self.overview.is_active() { + return false; + } + + if self.overview.select_direction(direction) { + crate::backends::direct::render_all(self); + } + + true + } + + pub(crate) fn select_overview_window(&mut self, id: WindowId) -> bool { + if !self.overview.is_active() || !self.overview.presentations.contains_key(&id) { + return false; + } + + if !self.activate_managed_window(id) { + return false; + } + + self.set_overview_active(false); + true + } + + pub(crate) fn retarget_overview(&mut self) { + if !self.overview.is_active() { + return; + } + + let targets = self.overview_targets(); + self.overview.retarget(targets, self.animations_enabled()); + } + + pub(crate) fn presented_window_rect(&self, id: WindowId) -> Option { + let normal = self.window_geometry.get(&id)?.visual.current; + + Some(self.overview.presented_rect(id, normal)) + } + + 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 source = geometry.client.committed_size?; + let presented = self.overview.presented_rect(id, geometry.visual.current); + + if source.width <= 0 + || source.height <= 0 + || presented.width <= 0.0 + || presented.height <= 0.0 + { + return None; + } + + let scale_x = presented.width / f64::from(source.width); + let scale_y = presented.height / f64::from(source.height); + + Some(((x - presented.x) / scale_x, (y - presented.y) / scale_y)) + } + + pub(crate) fn overview_selected(&self, id: WindowId) -> bool { + self.overview.is_active() && self.overview.selected() == Some(id) + } + + fn overview_targets(&self) -> HashMap { + let mut targets = HashMap::new(); + + for output in self.space.outputs() { + let Some(output_id) = self.output_id(output) else { + continue; + }; + let Some(workspace) = self.output_workspaces.active_workspace(output_id) else { + continue; + }; + let Some(bounds) = self.output_bounds_for(output) else { + continue; + }; + let mut windows = self + .window_ids + .values() + .filter(|id| self.workspaces.workspace_for_window(**id) == Some(workspace)) + .filter_map(|id| { + let normal = self.window_geometry.get(id)?.visual.current; + Some((*id, normal)) + }) + .collect::>(); + + windows.sort_by_key(|(id, _)| id.0); + for (id, target) in overview_layout(bounds, &windows) { + let normal = self.window_geometry[&id].visual.current; + targets.insert(id, (normal, target)); + } + } + + targets + } + + fn normal_window_rects(&self) -> HashMap { + self.window_geometry + .iter() + .map(|(id, geometry)| (*id, geometry.visual.current)) + .collect() + } +} + +fn overview_layout(bounds: Rect, windows: &[(WindowId, Rect)]) -> HashMap { + if windows.is_empty() { + return HashMap::new(); + } + + let content_width = (bounds.width - OVERVIEW_MARGIN * 2.0).max(1.0); + let content_height = (bounds.height - OVERVIEW_MARGIN * 2.0).max(1.0); + let aspect = (content_width / content_height).max(0.1); + let columns = ((windows.len() as f64 * aspect).sqrt().ceil() as usize).clamp(1, windows.len()); + let rows = windows.len().div_ceil(columns); + let cell_width = ((content_width - OVERVIEW_GAP * columns.saturating_sub(1) as f64) + / columns as f64) + .max(1.0); + let cell_height = + ((content_height - OVERVIEW_GAP * rows.saturating_sub(1) as f64) / rows as f64).max(1.0); + let origin_x = bounds.x + OVERVIEW_MARGIN; + let origin_y = bounds.y + OVERVIEW_MARGIN; + + windows + .iter() + .enumerate() + .map(|(index, (id, source))| { + let column = index % columns; + let row = index / columns; + let scale = (cell_width / source.width.max(1.0)) + .min(cell_height / source.height.max(1.0)) + .min(MAX_PREVIEW_SCALE); + let width = (source.width * scale).max(1.0); + let height = (source.height * scale).max(1.0); + let cell_x = origin_x + column as f64 * (cell_width + OVERVIEW_GAP); + let cell_y = origin_y + row as f64 * (cell_height + OVERVIEW_GAP); + let target = Rect::new( + cell_x + (cell_width - width) / 2.0, + cell_y + (cell_height - height) / 2.0, + width, + height, + ); + + (*id, target) + }) + .collect() +} + +fn rect_center(rect: Rect) -> (f64, f64) { + (rect.x + rect.width / 2.0, rect.y + rect.height / 2.0) +} + +fn directional_distance( + direction: Direction, + current: (f64, f64), + candidate: (f64, f64), +) -> Option { + let delta_x = candidate.0 - current.0; + let delta_y = candidate.1 - current.1; + let (primary, secondary) = match direction { + Direction::Left if delta_x < 0.0 => (-delta_x, delta_y.abs()), + Direction::Right if delta_x > 0.0 => (delta_x, delta_y.abs()), + Direction::Up if delta_y < 0.0 => (-delta_y, delta_x.abs()), + Direction::Down if delta_y > 0.0 => (delta_y, delta_x.abs()), + _ => return None, + }; + + Some(primary + secondary * 0.35) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn overview_grid_fits_windows_inside_the_output() { + let bounds = Rect::new(0.0, 0.0, 1920.0, 1080.0); + let windows = (1..=5) + .map(|id| (WindowId(id), Rect::new(0.0, 0.0, 960.0, 1000.0))) + .collect::>(); + let layout = overview_layout(bounds, &windows); + + assert_eq!(layout.len(), windows.len()); + assert!(layout.values().all(|rect| { + rect.x >= bounds.x + && rect.y >= bounds.y + && rect.x + rect.width <= bounds.x + bounds.width + && rect.y + rect.height <= bounds.y + bounds.height + })); + } + + #[test] + fn overview_grid_preserves_preview_aspect_ratio() { + let source = Rect::new(0.0, 0.0, 1200.0, 800.0); + let layout = overview_layout( + Rect::new(0.0, 0.0, 1920.0, 1080.0), + &[(WindowId(1), source)], + ); + let preview = layout[&WindowId(1)]; + + assert!((preview.width / preview.height - source.width / source.height).abs() < 0.001); + } + + #[test] + fn leaving_overview_keeps_presentations_until_they_settle() { + let id = WindowId(1); + let normal = Rect::new(0.0, 0.0, 1000.0, 800.0); + let target = Rect::new(200.0, 150.0, 500.0, 400.0); + let mut overview = OverviewState::default(); + + overview.enter(HashMap::from([(id, (normal, target))]), Some(id), false); + overview.exit(HashMap::from([(id, normal)]), true); + + assert_eq!(overview.presented_rect(id, normal), target); + assert!(!overview.is_active()); + assert!(overview.advance(Duration::from_millis(16), SpringConfig::default(), true)); + } + + #[test] + fn directional_selection_uses_presented_geometry() { + let left = WindowId(1); + let right = WindowId(2); + let left_rect = Rect::new(0.0, 0.0, 400.0, 400.0); + let right_rect = Rect::new(500.0, 0.0, 400.0, 400.0); + let mut overview = OverviewState::default(); + + overview.enter( + HashMap::from([ + (left, (left_rect, left_rect)), + (right, (right_rect, right_rect)), + ]), + Some(left), + false, + ); + + assert!(overview.select_direction(Direction::Right)); + assert_eq!(overview.selected(), Some(right)); + assert!(!overview.select_direction(Direction::Right)); + } +} diff --git a/compositor/src/shell_control.rs b/compositor/src/shell_control.rs index 74ea725..5be79d9 100644 --- a/compositor/src/shell_control.rs +++ b/compositor/src/shell_control.rs @@ -52,7 +52,7 @@ impl Dispatch for Ferese { let shell = data_init.init(id, ()); shell.capabilities(1); - shell.overview_state(u32::from(state.overview_active)); + shell.overview_state(u32::from(state.overview.is_active())); state.send_shell_snapshot(&shell); state.shell_resources.push(shell.downgrade()); } @@ -99,9 +99,7 @@ impl Dispatch for Ferese { } => { let id = join_id(window_hi, window_lo); - if state.activate_managed_window(id) { - state.set_overview_active(false); - } else { + if !state.select_overview_window(id) { send_request_failed( shell, ferese_shell_v1::FailedRequest::SelectOverviewWindow, @@ -218,19 +216,15 @@ impl Ferese { } } - fn set_overview_active(&mut self, active: bool) { - if self.overview_active == active { - return; - } + pub(crate) fn notify_overview_state(&mut self) { + let active = self.overview.is_active(); - self.overview_active = active; self.shell_resources.retain(|resource| { resource.upgrade().is_ok_and(|shell| { shell.overview_state(u32::from(active)); true }) }); - crate::backends::direct::render_all(self); } } diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 2995af0..21c38ca 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -122,6 +122,7 @@ pub struct Ferese { pub(crate) window_borders: HashMap, pub(crate) window_shadows: HashMap, pub(crate) rounded_clip_programs: HashMap, + pub(crate) overview_scrims: HashMap, pub(crate) material_programs: HashMap, pub(crate) backdrop_programs: HashMap, pub(crate) material_buffers: HashMap, @@ -160,7 +161,7 @@ pub struct Ferese { >, >, pub(crate) shell_snapshot_serial: u32, - pub(crate) overview_active: bool, + pub(crate) overview: crate::overview::OverviewState, next_window_id: u64, next_output_id: u64, last_animation_tick: Instant, @@ -259,6 +260,7 @@ impl Ferese { return; }; + self.overview_scrims.remove(&output_id); self.pending_screencopies.retain(|capture| { if capture.output == *output { capture.frame.failed(); @@ -446,6 +448,7 @@ impl Ferese { window_borders: HashMap::new(), window_shadows: HashMap::new(), rounded_clip_programs: HashMap::new(), + overview_scrims: HashMap::new(), material_programs: HashMap::new(), backdrop_programs: HashMap::new(), material_buffers: HashMap::new(), @@ -480,7 +483,7 @@ impl Ferese { pending_screencopies: Vec::new(), shell_resources: Vec::new(), shell_snapshot_serial: 0, - overview_active: false, + overview: crate::overview::OverviewState::default(), next_window_id: 1, next_output_id: 1, last_animation_tick: start_time, @@ -575,10 +578,13 @@ impl Ferese { &self, position: Point, ) -> Option<(WlSurface, Point)> { + if self.overview.is_active() { + return None; + } + self.space.elements().rev().find_map(|window| { let id = self.window_ids.get(window)?; - let geometry = self.window_geometry.get(id)?; - let visual = geometry.visual.current; + let visual = self.presented_window_rect(*id)?; let inside_visual = position.x >= visual.x && position.y >= visual.y && position.x < visual.x + visual.width @@ -587,7 +593,8 @@ impl Ferese { return None; } - let (source_x, source_y) = geometry.inverse_visual_point(position.x, position.y)?; + let (source_x, source_y) = + self.inverse_presented_window_point(*id, position.x, position.y)?; let source_point = Point::from((source_x, source_y)) + window.geometry().loc.to_f64(); window @@ -644,7 +651,7 @@ impl Ferese { pub fn window_under_visual(&self, position: Point) -> Option { self.space.elements().rev().find_map(|window| { let id = self.window_ids.get(window)?; - let visual = self.window_geometry.get(id)?.visual.current; + let visual = self.presented_window_rect(*id)?; (position.x >= visual.x && position.y >= visual.y @@ -681,7 +688,18 @@ impl Ferese { pub fn visual_scale_for_window(&self, window: &Window) -> Option<(f64, f64)> { let id = self.window_ids.get(window)?; - self.window_geometry.get(id)?.visual_scale() + let geometry = self.window_geometry.get(id)?; + let source = geometry.client.committed_size?; + let presented = self.presented_window_rect(*id)?; + + if source.width <= 0 || source.height <= 0 { + return None; + } + + Some(( + presented.width / f64::from(source.width), + presented.height / f64::from(source.height), + )) } pub(crate) fn visual_rect_for_window( @@ -689,7 +707,7 @@ impl Ferese { window: &Window, ) -> Option> { let id = self.window_ids.get(window)?; - let rect = self.window_geometry.get(id)?.visual.current; + let rect = self.presented_window_rect(*id)?; let size = ClientSize::from_rect(rect); Some(Rectangle::new( (rect.x.round() as i32, rect.y.round() as i32).into(), @@ -1143,6 +1161,7 @@ impl Ferese { } self.scrolling_world_x = scrolling_world_x; self.sync_window_stacking(); + self.retarget_overview(); self.send_shell_snapshots(); crate::backends::direct::render_all(self); @@ -1290,6 +1309,9 @@ impl Ferese { for id in settled_coupled_widths { self.viewport_coupled_widths.remove(&id); } + active_animation |= + self.overview + .advance(delta, self.spring_config, self.animations_enabled); self.sync_window_stacking(); if active_animation { @@ -1307,6 +1329,10 @@ impl Ferese { self.material_scene_generation } + pub(crate) fn animations_enabled(&self) -> bool { + self.animations_enabled + } + pub fn record_client_commit(&mut self, window: &Window) { let Some(id) = self.window_ids.get(window) else { return; @@ -1328,6 +1354,10 @@ impl Ferese { } pub fn focus_direction(&mut self, direction: Direction) { + if self.focus_overview_direction(direction) { + return; + } + let Some(current) = self.focused_window else { return; }; @@ -1798,7 +1828,7 @@ impl Ferese { self.output_bounds_for(output) } - fn output_bounds_for(&self, output: &Output) -> Option { + pub(crate) fn output_bounds_for(&self, output: &Output) -> Option { let geometry = self.space.output_geometry(output)?; let zone = layer_map_for_output(output).non_exclusive_zone(); diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 1daaa8d..8de867c 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -15,6 +15,7 @@ use smithay::{ UnderlyingStorage, memory::MemoryRenderBufferRenderElement, render_elements, + solid::{SolidColorBuffer, SolidColorRenderElement}, surface::{ WaylandSurfaceRenderElement, WaylandSurfaceTexture, render_elements_from_surface_tree, @@ -43,7 +44,7 @@ use smithay::{ reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind, reexports::wayland_server::Resource, utils::{ - Buffer, Clock, Logical, Monotonic, Physical, Point, Rectangle, Scale as RenderScale, + Buffer, Clock, Logical, Monotonic, Physical, Point, Rectangle, Scale as RenderScale, Size, Transform, }, wayland::shell::wlr_layer::Layer, @@ -74,6 +75,7 @@ render_elements! { Window=WindowRenderElement, Surface=SurfaceRenderElement, Memory=MemoryRenderElement, + Solid=SolidColorRenderElement, Border=PixelShaderElement, Backdrop=BackdropRenderElement, } @@ -310,6 +312,19 @@ pub(crate) struct RoundedClipPrograms { shadow: GlesPixelProgram, } +#[derive(Debug)] +pub(crate) struct OverviewScrim { + buffer: SolidColorBuffer, +} + +impl OverviewScrim { + fn new(size: Size) -> Self { + Self { + buffer: SolidColorBuffer::new(size, [0.02, 0.025, 0.035, 0.62]), + } + } +} + #[derive(Clone, Debug)] pub(crate) struct MaterialProgram(GlesPixelProgram); @@ -864,7 +879,7 @@ fn output_elements( .rev() .filter_map(|window| { let id = *state.window_ids.get(window)?; - let visual = state.window_geometry.get(&id)?.visual.current; + 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 @@ -885,7 +900,11 @@ 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 = state.focused_window == Some(id); + let focused = if state.overview.is_active() { + state.overview_selected(id) + } else { + state.focused_window == Some(id) + }; let border_width = if focused { state.theme_settings.focus_ring_width } else { @@ -964,6 +983,11 @@ fn output_elements( )); } } + if state.overview.is_presenting() + && let Some(scrim) = overview_scrim_element(state, output, output_geometry, scale) + { + elements.push(scrim.into()); + } elements.extend(layer_elements( state, renderer, @@ -973,6 +997,31 @@ fn output_elements( elements } +fn overview_scrim_element( + state: &mut Ferese, + output: &Output, + output_geometry: Rectangle, + scale: f64, +) -> Option { + let output_id = state.output_id(output)?; + let scrim = state + .overview_scrims + .entry(output_id) + .or_insert_with(|| OverviewScrim::new(output_geometry.size)); + + scrim + .buffer + .update(output_geometry.size, [0.02, 0.025, 0.035, 0.62]); + + Some(SolidColorRenderElement::from_buffer( + &scrim.buffer, + Point::::default(), + scale, + 1.0, + RenderElementKind::Unspecified, + )) +} + fn rounded_clip_program( state: &mut Ferese, renderer: &mut GlesRenderer,