Add centered workspace thumbnails to overview
Render a tinted, scrollable workspace strip with live thumbnails, output-scaled font labels and a subtle selected-workspace outline. Keep the window grid background-free and support workspace selection without exiting overview. Animate strip opacity independently with precise fade tolerances and continuous reversal, and expose toggle-overview through feresectl.
This commit is contained in:
1 parent
0709484efb
commit
b422448d88
11 files changed
+775
-52
No files matched your search
+226
-33
@@ -20,7 +20,7 @@ use smithay::{
|
||||
Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage,
|
||||
memory::MemoryRenderBufferRenderElement,
|
||||
render_elements,
|
||||
solid::{SolidColorBuffer, SolidColorRenderElement},
|
||||
solid::SolidColorRenderElement,
|
||||
surface::{
|
||||
WaylandSurfaceRenderElement, WaylandSurfaceTexture,
|
||||
render_elements_from_surface_tree,
|
||||
@@ -554,15 +554,7 @@ pub(crate) struct RoundedClipPrograms {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct OverviewScrim {
|
||||
buffer: SolidColorBuffer,
|
||||
}
|
||||
|
||||
impl OverviewScrim {
|
||||
fn new(size: Size<i32, Logical>) -> Self {
|
||||
Self {
|
||||
buffer: SolidColorBuffer::new(size, [0.02, 0.025, 0.035, 0.62]),
|
||||
}
|
||||
}
|
||||
chrome: HashMap<(u64, bool), HashMap<ErasedContextId, CachedBorder>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -1013,10 +1005,27 @@ fn output_elements(
|
||||
&[Layer::Overlay, Layer::Top]
|
||||
};
|
||||
elements.extend(layer_elements(state, renderer, output, upper_layers));
|
||||
let windows = state
|
||||
.space
|
||||
.elements()
|
||||
.rev()
|
||||
if state.overview.is_presenting() {
|
||||
elements.extend(overview_strip_elements(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
output_geometry,
|
||||
scale,
|
||||
));
|
||||
}
|
||||
let mut candidates = if state.overview.is_active() {
|
||||
state.window_ids.keys().cloned().collect::<Vec<_>>()
|
||||
} else {
|
||||
state.space.elements().rev().cloned().collect::<Vec<_>>()
|
||||
};
|
||||
if state.overview.is_active() {
|
||||
candidates.sort_by_key(|window| {
|
||||
std::cmp::Reverse(state.window_ids.get(window).map_or(0, |id| id.0))
|
||||
});
|
||||
}
|
||||
let windows = candidates
|
||||
.iter()
|
||||
.filter_map(|window| {
|
||||
let id = *state.window_ids.get(window)?;
|
||||
// Scrolling columns may sit outside their monitor's rectangle.
|
||||
@@ -1219,11 +1228,6 @@ 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,
|
||||
@@ -1236,29 +1240,218 @@ fn output_elements(
|
||||
elements
|
||||
}
|
||||
|
||||
fn overview_scrim_element(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn overview_chrome_element(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
output: &Output,
|
||||
output_geometry: Rectangle<i32, Logical>,
|
||||
scale: f64,
|
||||
) -> Option<SolidColorRenderElement> {
|
||||
key: (u64, bool),
|
||||
rect: ferese_layout::Rect,
|
||||
color: [f32; 4],
|
||||
width: f64,
|
||||
radius: f64,
|
||||
) -> Option<PhysicalShaderElement> {
|
||||
let output_id = state.output_id(output)?;
|
||||
let scrim = state
|
||||
let geometry = rounded_visual_rect(rect, output_geometry.loc);
|
||||
let physical = physical_rect(rect, output_geometry.loc, scale);
|
||||
let mode = output.current_mode()?;
|
||||
let parameters = BorderParameters {
|
||||
geometry,
|
||||
clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()),
|
||||
radius: (radius * scale) as f32,
|
||||
width: (width * scale) as f32,
|
||||
color,
|
||||
};
|
||||
let program = if key.1 {
|
||||
rounded_clip_program(state, renderer)?.border
|
||||
} else {
|
||||
material_program(state, renderer)?.0
|
||||
};
|
||||
let uniforms = |p: &BorderParameters| {
|
||||
if key.1 {
|
||||
border_uniforms(p)
|
||||
} else {
|
||||
vec![
|
||||
Uniform::new("visible_rect", p.clip_rect).into_owned(),
|
||||
Uniform::new("material_radius", p.radius).into_owned(),
|
||||
Uniform::new("tint", p.color).into_owned(),
|
||||
Uniform::new("paint_mode", 0.0_f32).into_owned(),
|
||||
Uniform::new("shadow_rect", p.clip_rect).into_owned(),
|
||||
Uniform::new("shadow_values", [0.0_f32; 2]).into_owned(),
|
||||
]
|
||||
}
|
||||
};
|
||||
let context = renderer.context_id().erased();
|
||||
let chrome = &mut state
|
||||
.overview_scrims
|
||||
.entry(output_id)
|
||||
.or_insert_with(|| OverviewScrim::new(output_geometry.size));
|
||||
.or_insert_with(|| OverviewScrim {
|
||||
chrome: HashMap::new(),
|
||||
})
|
||||
.chrome;
|
||||
let cached = chrome
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.entry(context)
|
||||
.or_insert_with(|| CachedBorder {
|
||||
element: PixelShaderElement::new(
|
||||
program,
|
||||
geometry,
|
||||
None,
|
||||
1.0,
|
||||
uniforms(¶meters),
|
||||
RenderElementKind::Unspecified,
|
||||
),
|
||||
parameters: parameters.clone(),
|
||||
});
|
||||
if cached.parameters != parameters {
|
||||
if cached.parameters.geometry != geometry {
|
||||
cached.element.resize(geometry, None);
|
||||
}
|
||||
cached.element.update_uniforms(uniforms(¶meters));
|
||||
cached.parameters = parameters;
|
||||
}
|
||||
Some(PhysicalShaderElement {
|
||||
inner: cached.element.clone(),
|
||||
geometry: physical,
|
||||
})
|
||||
}
|
||||
|
||||
scrim
|
||||
.buffer
|
||||
.update(output_geometry.size, [0.02, 0.025, 0.035, 0.62]);
|
||||
|
||||
Some(SolidColorRenderElement::from_buffer(
|
||||
&scrim.buffer,
|
||||
Point::<i32, Physical>::default(),
|
||||
fn overview_strip_elements(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
output: &Output,
|
||||
output_geometry: Rectangle<i32, Logical>,
|
||||
scale: f64,
|
||||
) -> Vec<AnimatedWindowRenderElement> {
|
||||
let alpha = state.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 mut elements = Vec::new();
|
||||
let programs = rounded_clip_program(state, renderer);
|
||||
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.
|
||||
if card.selected {
|
||||
let mut color = state.theme_settings.accent_color.0;
|
||||
color[3] *= alpha;
|
||||
if let Some(border) = overview_chrome_element(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
output_geometry,
|
||||
scale,
|
||||
(card.workspace.0, true),
|
||||
card.rect,
|
||||
color,
|
||||
1.0,
|
||||
10.0,
|
||||
) {
|
||||
elements.push(border.into());
|
||||
}
|
||||
}
|
||||
if let Some(programs) = &programs {
|
||||
for (id, rect) in &card.windows {
|
||||
if let Some(window) = state
|
||||
.window_ids
|
||||
.iter()
|
||||
.find_map(|(window, candidate)| (*candidate == *id).then(|| window.clone()))
|
||||
{
|
||||
elements.extend(rounded_window_elements(
|
||||
renderer,
|
||||
&window,
|
||||
rounded_visual_rect(*rect, output_geometry.loc),
|
||||
physical_rect(*rect, output_geometry.loc, scale),
|
||||
scale,
|
||||
4.0,
|
||||
alpha,
|
||||
true,
|
||||
output,
|
||||
programs.texture.clone(),
|
||||
ConstrainScaleBehavior::Stretch,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((buffer, size)) = state.overview_workspace_label(card.workspace, scale) {
|
||||
let location = Point::<i32, Physical>::from((
|
||||
((card.rect.x - f64::from(output_geometry.loc.x) + card.rect.width * 0.5) * scale)
|
||||
.round() as i32
|
||||
- size.w / 2,
|
||||
((card.rect.y - f64::from(output_geometry.loc.y) + card.rect.height - 20.0) * scale)
|
||||
.round() as i32,
|
||||
));
|
||||
if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
|
||||
renderer,
|
||||
location.to_f64(),
|
||||
&buffer,
|
||||
Some(alpha),
|
||||
None,
|
||||
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(card.rect, output_geometry.loc, scale),
|
||||
) {
|
||||
elements.push(element.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut card_color = state.theme_settings.surface_base_color.0;
|
||||
for component in &mut card_color[..3] {
|
||||
*component = (*component + 0.035).min(1.0);
|
||||
}
|
||||
card_color[3] = alpha * 0.75;
|
||||
if let Some(card_fill) = overview_chrome_element(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
output_geometry,
|
||||
scale,
|
||||
(card.workspace.0, false),
|
||||
card.rect,
|
||||
card_color,
|
||||
0.0,
|
||||
10.0,
|
||||
) {
|
||||
elements.push(card_fill.into());
|
||||
}
|
||||
}
|
||||
let mut color = state.theme_settings.surface_base_color.0;
|
||||
color[3] = alpha * state.theme_settings.panel_opacity as f32;
|
||||
if let Some(strip) = overview_chrome_element(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
output_geometry,
|
||||
scale,
|
||||
1.0,
|
||||
RenderElementKind::Unspecified,
|
||||
))
|
||||
(0, false),
|
||||
crate::overview::workspace_strip(bounds),
|
||||
color,
|
||||
0.0,
|
||||
16.0,
|
||||
) {
|
||||
elements.push(strip.into());
|
||||
}
|
||||
elements
|
||||
}
|
||||
|
||||
fn rounded_clip_program(
|
||||
|
||||
Reference in new issue
Block a user