diff --git a/compositor/src/config.rs b/compositor/src/config.rs index 9e35c52..49bd602 100644 --- a/compositor/src/config.rs +++ b/compositor/src/config.rs @@ -190,6 +190,7 @@ pub struct ThemeSettings { pub focus_ring_gradient: Option, pub shadow_color: RgbaColor, pub surface_base_color: RgbaColor, + pub text_primary_color: RgbaColor, pub shell_opacity: f64, pub inactive_dim: InactiveDimSettings, pub window_radius: f64, @@ -359,6 +360,8 @@ fn default_backdrop_blur() -> f64 { #[derive(Debug, Deserialize)] struct ThemeColorsConfig { + #[serde(default = "default_text_primary_color")] + text_primary: String, #[serde(default = "default_surface_base_color")] surface_base: String, #[serde(default = "default_border_color")] @@ -373,6 +376,7 @@ impl Default for ThemeColorsConfig { fn default() -> Self { Self { surface_base: default_surface_base_color(), + text_primary: default_text_primary_color(), border: default_border_color(), accent: default_accent_color(), shadow: default_shadow_color(), @@ -1081,6 +1085,10 @@ impl Config { border_width, focus_ring_width, border_color: parse_color(&self.theme.colors.border, "colors.border")?, + text_primary_color: parse_color( + &self.theme.colors.text_primary, + "colors.text_primary", + )?, accent_color: parse_color(&self.theme.colors.accent, "colors.accent")?, border_gradient: self.theme.border.settings("border")?, focus_ring_gradient: self.theme.focus_ring.settings("focus_ring")?, @@ -1670,6 +1678,10 @@ fn default_border_color() -> String { "#FFFFFF18".to_owned() } +fn default_text_primary_color() -> String { + "#F4F7FB".to_owned() +} + fn default_surface_base_color() -> String { "#111821".to_owned() } @@ -2025,6 +2037,7 @@ mod tests { focus_ring_gradient: None, shadow_color: RgbaColor([1.0 / 255.0, 2.0 / 255.0, 3.0 / 255.0, 128.0 / 255.0]), surface_base_color: RgbaColor([17.0 / 255.0, 24.0 / 255.0, 33.0 / 255.0, 1.0,]), + text_primary_color: RgbaColor([244.0 / 255.0, 247.0 / 255.0, 251.0 / 255.0, 1.0]), shell_opacity: 0.78, inactive_dim: InactiveDimSettings { enabled: false, diff --git a/compositor/src/input.rs b/compositor/src/input.rs index b64c651..f0b6267 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -135,6 +135,17 @@ impl Ferese { } let symbol = keysym.modified_sym().raw(); + if data.overview.is_active() + && matches!(symbol, keysyms::KEY_Return | keysyms::KEY_KP_Enter) + { + if state == KeyState::Pressed { + data.intercepted_keys.insert(keycode); + if let Some(id) = data.overview.selected() { + data.select_overview_window(id); + } + } + return FilterResult::Intercept(()); + } if overview_escape(symbol, data.overview.is_active()) { if state == KeyState::Pressed { data.intercepted_keys.insert(keycode); @@ -598,6 +609,10 @@ impl Ferese { if self.session_lock.active { return; } + if self.overview.is_active() { + self.hover_overview_window(position); + return; + } // Keep the explicitly selected window focused until Overview's exit // animation settles. Moving previews must not steal focus on pointer jitter. if self.overview.is_presenting() diff --git a/compositor/src/overview.rs b/compositor/src/overview.rs index 59a24c4..32bdcb3 100644 --- a/compositor/src/overview.rs +++ b/compositor/src/overview.rs @@ -20,7 +20,8 @@ use smithay::{ }; const OVERVIEW_MARGIN: f64 = 48.0; -const OVERVIEW_GAP: f64 = 24.0; +const OVERVIEW_GAP: f64 = 40.0; +const EXIT_DURATION: f64 = 0.18; const WORKSPACE_CARD_GAP: f64 = 8.0; const MAX_PREVIEW_SCALE: f64 = 0.82; @@ -97,12 +98,13 @@ pub(crate) struct OverviewState { selected: Option, presentations: HashMap, opacity: AnimatedValue, + exit_transition: Option<(f64, f64)>, strip_offsets: HashMap, font_family: String, font_requests: Option, font: Option, labels: HashMap< - (String, u64), + (String, u64, [u32; 4]), ( MemoryRenderBuffer, smithay::utils::Size, @@ -117,6 +119,7 @@ impl Default for OverviewState { selected: None, presentations: HashMap::new(), opacity: AnimatedValue::new(0.0), + exit_transition: None, strip_offsets: HashMap::new(), font_family: "sans-serif".into(), font_requests: None, @@ -134,6 +137,14 @@ pub(crate) struct WorkspaceCard { pub windows: Vec<(WindowId, Rect)>, } +impl WorkspaceCard { + fn window_at(&self, point: Point) -> Option { + self.windows + .iter() + .find_map(|(id, rect)| contains(*rect, point).then_some(*id)) + } +} + pub(crate) fn workspace_strip(bounds: Rect) -> Rect { let inset = 24.0_f64.min(bounds.width * 0.05); Rect::new( @@ -204,11 +215,12 @@ impl OverviewState { &mut self, text: &str, scale: f64, + color: [f32; 4], ) -> Option<( MemoryRenderBuffer, smithay::utils::Size, )> { - let key = (text.to_owned(), scale.to_bits()); + let key = (text.to_owned(), scale.to_bits(), color.map(f32::to_bits)); if let Some(buffer) = self.labels.get(&key) { return Some(buffer.clone()); } @@ -238,8 +250,13 @@ impl OverviewState { let y = y as i32 + bounds.min.y as i32; if x >= 0 && x < width && y >= 0 && y < height { let offset = ((y * width + x) * 4) as usize; - let alpha = (coverage * 255.0).round() as u8; - pixels[offset..offset + 4].copy_from_slice(&[alpha, alpha, alpha, alpha]); + let alpha = coverage * color[3]; + pixels[offset..offset + 4].copy_from_slice(&[ + (color[0] * alpha * 255.0).round() as u8, + (color[1] * alpha * 255.0).round() as u8, + (color[2] * alpha * 255.0).round() as u8, + (alpha * 255.0).round() as u8, + ]); } }); } @@ -308,9 +325,17 @@ impl OverviewState { } pub(crate) fn presented_rect(&self, id: WindowId, normal: Rect) -> Rect { - self.presentations - .get(&id) - .map_or(normal, |presentation| presentation.current) + 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 } fn enter( @@ -319,6 +344,19 @@ impl OverviewState { selected: Option, animations_enabled: bool, ) { + // Reverse a dismissal from the currently visible geometry, not the old grid. + if self.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) { + *presentation = AnimatedRect::new(rect); + } + } + } + self.exit_transition = None; self.active = true; self.opacity.retarget_preserving_motion(1.0); if !animations_enabled { @@ -355,6 +393,7 @@ impl OverviewState { 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); if !animations_enabled { self.opacity.snap(); @@ -383,6 +422,23 @@ impl OverviewState { 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() { @@ -417,6 +473,53 @@ impl OverviewState { } impl Ferese { + pub(crate) fn hover_overview_window(&mut self, point: Point) { + if !self.overview.is_active() { + return; + } + let selected = self + .window_under_visual(point) + .and_then(|w| self.window_ids.get(&w).copied()); + if selected.is_some() && self.overview.selected != selected { + self.overview.selected = selected; + crate::backends::direct::render_all(self); + } + } + + pub(crate) fn overview_window_label( + &mut self, + window: &smithay::desktop::Window, + scale: f64, + width: f64, + ) -> Option<( + MemoryRenderBuffer, + smithay::utils::Size, + )> { + use smithay::wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData}; + let surface = window.toplevel()?.wl_surface(); + let title = with_states(surface, |states| { + let data = states + .data_map + .get::()? + .lock() + .ok()?; + data.title + .clone() + .filter(|title| !title.trim().is_empty()) + .or_else(|| data.app_id.clone()) + }) + .unwrap_or_else(|| "Window".into()); + let limit = ((width - 16.0) / 8.0).clamp(1.0, 48.0) as usize; + let mut chars = title.chars().filter(|c| !c.is_control()); + let mut title: String = chars.by_ref().take(limit).collect(); + if chars.next().is_some() { + title.pop(); + title.push('…'); + } + self.overview + .label(&title, scale, self.theme_settings.text_primary_color.0) + } + pub(crate) fn overview_workspace_label( &mut self, workspace: WorkspaceId, @@ -426,7 +529,8 @@ impl Ferese { smithay::utils::Size, )> { let name = self.workspaces.workspace(workspace)?.name.clone(); - self.overview.label(&name, scale) + self.overview + .label(&name, scale, self.theme_settings.text_primary_color.0) } pub(crate) fn set_overview_active(&mut self, active: bool) { @@ -618,6 +722,12 @@ impl Ferese { .into_iter() .find(|card| contains(card.rect, point)); if let Some(card) = card { + if let Some(id) = card.window_at(point) { + if self.activate_managed_window(id) { + self.set_overview_active(false); + } + return true; + } let Some(output_id) = self.output_id(&output) else { return true; }; @@ -686,17 +796,19 @@ impl Ferese { 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)) + let Some(workspace) = self.workspaces.workspace(workspace) else { + continue; + }; + let windows = workspace + .layout + .window_ids() + .chain(workspace.floating.iter().copied()) .filter_map(|id| { - let normal = self.window_geometry.get(id)?.visual.current; - Some((*id, normal)) + 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(window_area(bounds), &windows) { let normal = self.window_geometry[&id].visual.current; targets.insert(id, (normal, target)); @@ -731,15 +843,43 @@ fn preview_layout( let margin = margin.min(bounds.width * 0.1).min(bounds.height * 0.1); let content_width = (bounds.width - margin * 2.0).max(1.0); let content_height = (bounds.height - 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()); + // Choose the grid from the actual window shapes. Three tall windows should + // share one row instead of becoming tiny previews spread over two rows. + let cell_size = |columns: usize| { + let rows = windows.len().div_ceil(columns); + ( + ((content_width - gap * columns.saturating_sub(1) as f64) / columns as f64) + .clamp(1.0, 480.0), + ((content_height - gap * rows.saturating_sub(1) as f64) / rows as f64) + .clamp(1.0, 420.0), + ) + }; + let score = |columns: usize| { + let (w, h) = cell_size(columns); + windows + .iter() + .map(|(_, source)| { + let scale = (w / source.width.max(1.0)) + .min(h / source.height.max(1.0)) + .min(MAX_PREVIEW_SCALE); + source.width * source.height * scale * scale + }) + .sum::() + }; + let columns = (1..=windows.len()) + .max_by(|a, b| { + let (sa, sb) = (score(*a), score(*b)); + if (sa - sb).abs() < 1.0 { + windows.len().div_ceil(*b).cmp(&windows.len().div_ceil(*a)) + } else { + sa.total_cmp(&sb) + } + }) + .unwrap_or(1); let rows = windows.len().div_ceil(columns); - let cell_width = - ((content_width - gap * columns.saturating_sub(1) as f64) / columns as f64).max(1.0); - let cell_height = - ((content_height - gap * rows.saturating_sub(1) as f64) / rows as f64).max(1.0); - let origin_x = bounds.x + margin; - let origin_y = bounds.y + margin; + let (cell_width, cell_height) = cell_size(columns); + let grid_height = cell_height * rows as f64 + gap * rows.saturating_sub(1) as f64; + let origin_y = bounds.y + (bounds.height - grid_height) * 0.5; windows .iter() @@ -752,7 +892,11 @@ fn preview_layout( .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 + gap); + let row_count = (windows.len() - row * columns).min(columns); + let row_width = + cell_width * row_count as f64 + gap * row_count.saturating_sub(1) as f64; + let cell_x = + bounds.x + (bounds.width - row_width) * 0.5 + column as f64 * (cell_width + gap); let cell_y = origin_y + row as f64 * (cell_height + gap); let target = Rect::new( cell_x + (cell_width - width) / 2.0, @@ -800,7 +944,7 @@ mod tests { fn font_reload_keeps_labels_until_current_result_arrives() { let mut overview = OverviewState::with_font_family("first".into()); assert!(overview.complete_font_load("first", Some(test_font()))); - assert!(overview.label("1", 1.0).is_some()); + assert!(overview.label("1", 1.0, [1.0; 4]).is_some()); assert_eq!(overview.labels.len(), 1); overview.set_font_family("second".into()); assert!(overview.font.is_some()); @@ -810,7 +954,7 @@ mod tests { assert_eq!(overview.labels.len(), 1); assert!(overview.complete_font_load("second", Some(test_font()))); assert!(overview.labels.is_empty()); - assert!(overview.label("1", 1.0).is_some()); + assert!(overview.label("1", 1.0, [1.0; 4]).is_some()); } #[test] @@ -836,7 +980,7 @@ mod tests { #[test] fn labels_do_not_load_fonts_synchronously() { let mut overview = OverviewState::default(); - assert!(overview.label("1", 1.0).is_none()); + assert!(overview.label("1", 1.0, [1.0; 4]).is_none()); assert!(overview.font.is_none()); assert!(overview.labels.is_empty()); } @@ -951,6 +1095,21 @@ mod tests { })); } + #[test] + fn tall_windows_share_a_row_when_their_shapes_fit() { + let bounds = Rect::new(0.0, 0.0, 1192.0, 1100.0); + let windows = (1..=3) + .map(|id| (WindowId(id), Rect::new(0.0, 0.0, 586.0, 1250.0))) + .collect::>(); + let grid = overview_layout(bounds, &windows); + let first = grid[&WindowId(1)]; + for rect in grid.values() { + assert_eq!(rect.y, first.y); + assert!(rect.height >= 400.0); + assert!(rect.x >= 0.0 && rect.x + rect.width <= bounds.width); + } + } + #[test] fn overview_grid_preserves_preview_aspect_ratio() { let source = Rect::new(0.0, 0.0, 1200.0, 800.0); @@ -963,6 +1122,54 @@ mod tests { assert!((preview.width / preview.height - source.width / source.height).abs() < 0.001); } + #[test] + fn exiting_overview_tracks_a_moving_layout_and_finishes_in_180ms() { + let id = WindowId(1); + let initial = Rect::new(-400.0, 0.0, 800.0, 600.0); + let preview = Rect::new(100.0, 100.0, 400.0, 300.0); + let moved = Rect::new(200.0, 0.0, 800.0, 600.0); + let mut overview = OverviewState::default(); + overview.enter(HashMap::from([(id, (initial, preview))]), Some(id), false); + overview.exit(HashMap::from([(id, initial)]), true); + overview.advance(Duration::from_millis(90), SpringConfig::default(), true); + let shown = overview.presented_rect(id, moved); + assert!((shown.x - 187.5).abs() < 0.001); + overview.advance(Duration::from_millis(90), SpringConfig::default(), true); + assert!(!overview.is_presenting()); + assert_eq!(overview.presented_rect(id, moved), moved); + } + + #[test] + fn reversing_exit_preserves_the_visible_window_position() { + let id = WindowId(1); + let normal = Rect::new(0.0, 0.0, 800.0, 600.0); + let preview = Rect::new(100.0, 100.0, 400.0, 300.0); + let targets = HashMap::from([(id, (normal, preview))]); + let mut overview = OverviewState::default(); + overview.enter(targets.clone(), Some(id), false); + overview.exit(HashMap::from([(id, normal)]), true); + overview.advance(Duration::from_millis(60), SpringConfig::default(), true); + let shown = overview.presented_rect(id, normal); + overview.enter(targets, Some(id), true); + assert_eq!(overview.presented_rect(id, normal), shown); + } + + #[test] + fn workspace_thumbnail_hit_selects_the_clicked_window_not_workspace_focus() { + let card = WorkspaceCard { + workspace: WorkspaceId(1), + rect: Rect::new(0.0, 0.0, 160.0, 100.0), + selected: true, + windows: vec![ + (WindowId(1), Rect::new(5.0, 5.0, 70.0, 65.0)), + (WindowId(2), Rect::new(85.0, 5.0, 70.0, 65.0)), + ], + }; + assert_eq!(card.window_at((120.0, 35.0).into()), Some(WindowId(2))); + assert_eq!(card.window_at((30.0, 35.0).into()), Some(WindowId(1))); + assert_eq!(card.window_at((80.0, 85.0).into()), None); + } + #[test] fn leaving_overview_keeps_presentations_until_they_settle() { let id = WindowId(1); diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 8e37e03..e6014fd 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -837,11 +837,17 @@ impl Ferese { pub fn window_under_visual(&self, position: Point) -> Option { let workspace = self.workspace_under_pointer(position)?; - let candidates = if self.overview.is_active() { + let mut candidates = if self.overview.is_active() { self.window_ids.keys().cloned().collect::>() } else { self.space.elements().rev().cloned().collect::>() }; + if self.overview.is_active() { + // 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.iter().find_map(|window| { if !self.window_content_ready(window) { return None; @@ -852,10 +858,11 @@ impl Ferese { } let visual = self.presented_window_rect(*id)?; + let caption_height = if self.overview.is_active() { 34.0 } else { 0.0 }; (position.x >= visual.x && position.y >= visual.y && position.x < visual.x + visual.width - && position.y < visual.y + visual.height) + && position.y < visual.y + visual.height + caption_height) .then(|| window.clone()) }) } diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 0020287..4ce14df 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -568,9 +568,17 @@ pub(crate) struct RoundedClipPrograms { shadow: GlesPixelProgram, } +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) enum OverviewChromePart { + Card, + Outline, + Strip, + Caption, +} + #[derive(Debug)] pub(crate) struct OverviewScrim { - chrome: HashMap<(u64, bool), HashMap>, + chrome: HashMap<(u64, OverviewChromePart), HashMap>, } #[derive(Clone, Debug)] @@ -1334,7 +1342,7 @@ fn overview_chrome_element( output: &Output, output_geometry: Rectangle, scale: f64, - key: (u64, bool), + key: (u64, OverviewChromePart), rect: ferese_layout::Rect, color: [f32; 4], width: f64, @@ -1357,13 +1365,13 @@ fn overview_chrome_element( focus_gradient_line: [0.0; 4], focus_mix: 0.0, }; - let program = if key.1 { + let program = if key.1 == OverviewChromePart::Outline { rounded_clip_program(state, renderer)?.border } else { material_program(state, renderer)?.0 }; let uniforms = |p: &BorderParameters| { - if key.1 { + if key.1 == OverviewChromePart::Outline { border_uniforms(p) } else { vec![ @@ -1427,6 +1435,23 @@ fn overview_strip_elements( return Vec::new(); }; let cards = state.overview_workspace_cards(output); + let strip = crate::overview::workspace_strip(bounds); + let panel = match (cards.first(), cards.last()) { + (Some(first), Some(last)) => ferese_layout::Rect::new( + first.rect.x - 12.0, + strip.y, + last.rect.x + last.rect.width - first.rect.x + 24.0, + strip.height, + ), + _ => strip, + }; + let radius = state.theme_settings.material_radius; + let material_opacity = crate::effects::resolve_material( + crate::effects::SemanticRole::Panel, + state.theme_settings.material_style, + state.theme_settings.shell_opacity as f32, + ) + .opacity; let mut elements = Vec::new(); let programs = rounded_clip_program(state, renderer); // Resolve IDs once per strip, rather than scanning all managed windows @@ -1436,6 +1461,86 @@ fn overview_strip_elements( .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) + { + cache.chrome.retain(|(id, part), _| match part { + OverviewChromePart::Caption => windows_by_id.keys().any(|window| window.0 == *id), + OverviewChromePart::Card | OverviewChromePart::Outline => { + cards.iter().any(|card| card.workspace.0 == *id) + } + OverviewChromePart::Strip => true, + }); + } + // Cached title textures are independent of window content and stay readable + // when thumbnails are small. Their pills use the shell's material and radius. + for (id, window) in &windows_by_id { + if !state.window_belongs_to_output(*id, output) { + continue; + } + let Some(rect) = state.presented_window_rect(*id) else { + continue; + }; + let Some((buffer, size)) = state.overview_window_label(window, scale, rect.width) else { + continue; + }; + let width = (f64::from(size.w) / scale + 16.0).min(rect.width); + let caption = ferese_layout::Rect::new( + rect.x + (rect.width - width) * 0.5, + rect.y + rect.height + 8.0, + width, + 26.0, + ); + let location = Point::::from(( + ((caption.x - f64::from(output_geometry.loc.x) + caption.width * 0.5) * scale).round() + as i32 + - size.w / 2, + ((caption.y - f64::from(output_geometry.loc.y) + 4.0) * scale).round() as i32, + )); + if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer( + renderer, + location.to_f64(), + &buffer, + Some(alpha), + Some(Rectangle::from_size( + (f64::from(size.w), f64::from(size.h)).into(), + )), + Some( + ( + (f64::from(size.w) / scale).round() as i32, + (f64::from(size.h) / scale).round() as i32, + ) + .into(), + ), + RenderElementKind::Unspecified, + ) { + let element = RescaleRenderElement::from_element(element, location, 1.0); + let element = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative); + if let Some(element) = CropRenderElement::from_element( + element, + scale, + physical_rect(caption, output_geometry.loc, scale), + ) { + elements.push(element.into()); + } + } + let mut fill = state.theme_settings.surface_base_color.0; + fill[3] = alpha * material_opacity; + if let Some(background) = overview_chrome_element( + state, + renderer, + output, + output_geometry, + scale, + (id.0, OverviewChromePart::Caption), + caption, + fill, + 0.0, + radius.min(13.0), + ) { + elements.push(background.into()); + } + } for card in cards { // Front-to-back: outline and live miniatures above each card, all above // the strip. Nothing is painted behind the main overview window grid. @@ -1448,11 +1553,11 @@ fn overview_strip_elements( output, output_geometry, scale, - (card.workspace.0, true), + (card.workspace.0, OverviewChromePart::Outline), card.rect, color, 1.0, - 10.0, + radius, ) { elements.push(border.into()); } @@ -1489,7 +1594,9 @@ fn overview_strip_elements( location.to_f64(), &buffer, Some(alpha), - None, + Some(Rectangle::from_size( + (f64::from(size.w), f64::from(size.h)).into(), + )), Some( ( (f64::from(size.w) / scale).round() as i32, @@ -1515,39 +1622,41 @@ fn overview_strip_elements( for component in &mut card_color[..3] { *component = (*component + 0.035).min(1.0); } - card_color[3] = alpha * 0.75; + card_color[3] = alpha * material_opacity; if let Some(card_fill) = overview_chrome_element( state, renderer, output, output_geometry, scale, - (card.workspace.0, false), + (card.workspace.0, OverviewChromePart::Card), card.rect, card_color, 0.0, - 10.0, + radius, ) { elements.push(card_fill.into()); } } let mut color = state.theme_settings.surface_base_color.0; - color[3] = alpha * crate::effects::resolve_material( - crate::effects::SemanticRole::Panel, - state.theme_settings.material_style, - state.theme_settings.shell_opacity as f32, - ).opacity; + color[3] = alpha + * crate::effects::resolve_material( + crate::effects::SemanticRole::Panel, + state.theme_settings.material_style, + state.theme_settings.shell_opacity as f32, + ) + .opacity; if let Some(strip) = overview_chrome_element( state, renderer, output, output_geometry, scale, - (0, false), - crate::overview::workspace_strip(bounds), + (0, OverviewChromePart::Strip), + panel, color, 0.0, - 16.0, + radius, ) { elements.push(strip.into()); } diff --git a/docs/configuration.md b/docs/configuration.md index 89f9f46..3b6b440 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -210,6 +210,16 @@ theme { | `repeat_rate` | integer > 0 | `25` | Repeats per second | | `repeat_delay_ms` | integer ≥ 0 | `600` | Delay before repeat | +With focus-follows-mouse enabled, hovering a visible part of a window changes +keyboard focus without raising it or moving the scrolling viewport. Clicking or +using directional keyboard focus brings the selected window into view. + +In Overview (`Super+Tab`), click a window preview or a miniature inside a workspace +card to activate that exact window. Hover highlights the target; directional +focus keys move the selection, Enter activates it, and Escape dismisses Overview. +Clicking a workspace card's background switches workspaces while keeping Overview +open. Previews follow layout order and use the configured shell font and colors. + The touchpad device keys are booleans, all defaulting to `true`: `tap`, `natural_scroll` and `disable_while_typing`. These apply to DRM devices; nested previews use the host's physical input settings.