Make Overview selection responsive and simplify window previews

This commit is contained in:
Samuel Onoja committed 2026-09-28 10:45:38 +01:00
1 parent 442b950bcd
commit 41d63f6b18
6 files changed
+406 -45

No files matched your search

+13
View File
@@ -190,6 +190,7 @@ pub struct ThemeSettings {
pub focus_ring_gradient: Option<BorderGradient>,
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,
+15
View File
@@ -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()
+235 -28
View File
@@ -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<WindowId>,
presentations: HashMap<WindowId, AnimatedRect>,
opacity: AnimatedValue,
exit_transition: Option<(f64, f64)>,
strip_offsets: HashMap<OutputId, usize>,
font_family: String,
font_requests: Option<FontRequests>,
font: Option<FontArc>,
labels: HashMap<
(String, u64),
(String, u64, [u32; 4]),
(
MemoryRenderBuffer,
smithay::utils::Size<i32, smithay::utils::Buffer>,
@@ -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<f64, Logical>) -> Option<WindowId> {
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<i32, smithay::utils::Buffer>,
)> {
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<WindowId>,
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<WindowId, Rect>, 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<f64, Logical>) {
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<i32, smithay::utils::Buffer>,
)> {
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::<XdgToplevelSurfaceData>()?
.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<i32, smithay::utils::Buffer>,
)> {
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::<Vec<_>>();
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);
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;
(
((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::<f64>()
};
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, 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::<Vec<_>>();
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);
+9 -2
View File
@@ -837,11 +837,17 @@ impl Ferese {
pub fn window_under_visual(&self, position: Point<f64, Logical>) -> Option<Window> {
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::<Vec<_>>()
} else {
self.space.elements().rev().cloned().collect::<Vec<_>>()
};
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())
})
}
+124 -15
View File
@@ -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<ErasedContextId, CachedBorder>>,
chrome: HashMap<(u64, OverviewChromePart), HashMap<ErasedContextId, CachedBorder>>,
}
#[derive(Clone, Debug)]
@@ -1334,7 +1342,7 @@ fn overview_chrome_element(
output: &Output,
output_geometry: Rectangle<i32, Logical>,
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::<i32, Physical>::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(
color[3] = alpha
* crate::effects::resolve_material(
crate::effects::SemanticRole::Panel,
state.theme_settings.material_style,
state.theme_settings.shell_opacity as f32,
).opacity;
)
.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());
}
+10
View File
@@ -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.