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
Generated
+2
@@ -1570,12 +1570,14 @@ dependencies = [
|
|||||||
name = "ferese"
|
name = "ferese"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ab_glyph",
|
||||||
"calloop 0.14.4",
|
"calloop 0.14.4",
|
||||||
"ferese-animation",
|
"ferese-animation",
|
||||||
"ferese-core",
|
"ferese-core",
|
||||||
"ferese-ipc",
|
"ferese-ipc",
|
||||||
"ferese-layout",
|
"ferese-layout",
|
||||||
"ferese-protocols",
|
"ferese-protocols",
|
||||||
|
"fontdb",
|
||||||
"image",
|
"image",
|
||||||
"libc",
|
"libc",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -198,6 +198,15 @@ out over 80 ms, with a 64 MiB total snapshot budget and automatic release.
|
|||||||
All monitors share one elapsed-time animation clock, and window content,
|
All monitors share one elapsed-time animation clock, and window content,
|
||||||
clips, borders and shadows use the same physical-pixel edges.
|
clips, borders and shadows use the same physical-pixel edges.
|
||||||
|
|
||||||
|
Overview has a horizontally scrollable workspace strip with live window
|
||||||
|
thumbnails and an accent outline on the workspace shown by each output.
|
||||||
|
Click a workspace to change the window grid without leaving overview; click
|
||||||
|
a window to focus it and exit, or press Escape to dismiss. Only the workspace
|
||||||
|
strip has a tinted surface; the window grid sits directly over the wallpaper.
|
||||||
|
Strip labels use `[theme.typography].font_family` and rasterize at output scale.
|
||||||
|
The strip fades independently and preserves its opacity on rapid reversals,
|
||||||
|
including when the selected workspace is empty.
|
||||||
|
|
||||||
Ferese renders `[theme.background]` wallpapers directly: `path` selects the
|
Ferese renders `[theme.background]` wallpapers directly: `path` selects the
|
||||||
image and `mode = "fill"` or `"fit"` controls crop/letterboxing. One decoded
|
image and `mode = "fill"` or `"fit"` controls crop/letterboxing. One decoded
|
||||||
image and one texture per GPU context serve every output; resizing requires
|
image and one texture per GPU context serve every output; resizing requires
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ license.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ab_glyph = "0.2.32"
|
||||||
|
fontdb = "0.23.0"
|
||||||
calloop.workspace = true
|
calloop.workspace = true
|
||||||
ferese-animation = { path = "../crates/ferese-animation" }
|
ferese-animation = { path = "../crates/ferese-animation" }
|
||||||
ferese-core = { path = "../crates/ferese-core" }
|
ferese-core = { path = "../crates/ferese-core" }
|
||||||
|
|||||||
@@ -167,6 +167,8 @@ impl Default for InactiveDimConfig {
|
|||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
struct ThemeConfig {
|
struct ThemeConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
typography: OverviewTypographyConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
background: crate::wallpaper::WallpaperConfig,
|
background: crate::wallpaper::WallpaperConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -182,11 +184,23 @@ struct ThemeConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
pub(crate) fn overview_font_family(&self) -> String {
|
||||||
|
self.theme
|
||||||
|
.typography
|
||||||
|
.font_family
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "sans-serif".into())
|
||||||
|
}
|
||||||
pub(crate) fn wallpaper_settings(&self) -> crate::wallpaper::WallpaperConfig {
|
pub(crate) fn wallpaper_settings(&self) -> crate::wallpaper::WallpaperConfig {
|
||||||
self.theme.background.clone()
|
self.theme.background.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
struct OverviewTypographyConfig {
|
||||||
|
font_family: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
struct ThemeSurfaceConfig {
|
struct ThemeSurfaceConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -195,6 +195,9 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if event.state() == ButtonState::Pressed {
|
if event.state() == ButtonState::Pressed {
|
||||||
|
if self.click_overview_workspace(position) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(window) = self.window_under_visual(position)
|
if let Some(window) = self.window_under_visual(position)
|
||||||
&& let Some(id) = self.window_ids.get(&window).copied()
|
&& let Some(id) = self.window_ids.get(&window).copied()
|
||||||
{
|
{
|
||||||
@@ -230,6 +233,17 @@ impl Ferese {
|
|||||||
let vertical = event.amount(Axis::Vertical).unwrap_or_else(|| {
|
let vertical = event.amount(Axis::Vertical).unwrap_or_else(|| {
|
||||||
event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0
|
event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0
|
||||||
});
|
});
|
||||||
|
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||||
|
if self.scroll_overview_strip(
|
||||||
|
pointer.current_location(),
|
||||||
|
if horizontal != 0.0 {
|
||||||
|
horizontal
|
||||||
|
} else {
|
||||||
|
vertical
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut frame = AxisFrame::new(event.time() as u32).source(source);
|
let mut frame = AxisFrame::new(event.time() as u32).source(source);
|
||||||
if horizontal != 0.0 {
|
if horizontal != 0.0 {
|
||||||
frame = frame.value(Axis::Horizontal, horizontal);
|
frame = frame.value(Axis::Horizontal, horizontal);
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ impl Ferese {
|
|||||||
"toggle-fullscreen" => self.toggle_focused_fullscreen(),
|
"toggle-fullscreen" => self.toggle_focused_fullscreen(),
|
||||||
"toggle-maximized" => self.toggle_focused_maximized(),
|
"toggle-maximized" => self.toggle_focused_maximized(),
|
||||||
"toggle-layout" => self.toggle_layout_mode(),
|
"toggle-layout" => self.toggle_layout_mode(),
|
||||||
|
"toggle-overview" => self.toggle_overview(),
|
||||||
"cycle-column-width" => self.cycle_focused_column_width(),
|
"cycle-column-width" => self.cycle_focused_column_width(),
|
||||||
"center-column" => self.center_focused_column(),
|
"center-column" => self.center_focused_column(),
|
||||||
"consume" => self.consume_focused_window(),
|
"consume" => self.consume_focused_window(),
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
viewport_spring_config: config.viewport_spring_config()?,
|
viewport_spring_config: config.viewport_spring_config()?,
|
||||||
output_profiles,
|
output_profiles,
|
||||||
wallpaper: config.wallpaper_settings(),
|
wallpaper: config.wallpaper_settings(),
|
||||||
|
overview_font_family: config.overview_font_family(),
|
||||||
};
|
};
|
||||||
let mut state = Ferese::new(&mut event_loop, display, runtime)?;
|
let mut state = Ferese::new(&mut event_loop, display, runtime)?;
|
||||||
backends::init(launch.backend, &mut event_loop, &mut state)?;
|
backends::init(launch.backend, &mut event_loop, &mut state)?;
|
||||||
|
|||||||
+486
-14
@@ -1,28 +1,206 @@
|
|||||||
use std::{collections::HashMap, time::Duration};
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
|
||||||
use ferese_animation::{AnimatedRect, SpringConfig};
|
use ferese_animation::{AnimatedRect, AnimatedValue, SpringConfig};
|
||||||
|
use ferese_core::{OutputId, WorkspaceId};
|
||||||
use ferese_layout::{Direction, Rect, WindowId};
|
use ferese_layout::{Direction, Rect, WindowId};
|
||||||
|
use smithay::{
|
||||||
|
output::Output,
|
||||||
|
utils::{Logical, Point},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::Ferese;
|
use crate::Ferese;
|
||||||
|
use ab_glyph::{Font, FontArc, FontVec, ScaleFont};
|
||||||
|
use smithay::{
|
||||||
|
backend::{allocator::Fourcc, renderer::element::memory::MemoryRenderBuffer},
|
||||||
|
utils::Transform,
|
||||||
|
};
|
||||||
|
|
||||||
const OVERVIEW_MARGIN: f64 = 48.0;
|
const OVERVIEW_MARGIN: f64 = 48.0;
|
||||||
const OVERVIEW_GAP: f64 = 24.0;
|
const OVERVIEW_GAP: f64 = 24.0;
|
||||||
|
const WORKSPACE_CARD_GAP: f64 = 8.0;
|
||||||
const MAX_PREVIEW_SCALE: f64 = 0.82;
|
const MAX_PREVIEW_SCALE: f64 = 0.82;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct OverviewState {
|
pub(crate) struct OverviewState {
|
||||||
active: bool,
|
active: bool,
|
||||||
selected: Option<WindowId>,
|
selected: Option<WindowId>,
|
||||||
presentations: HashMap<WindowId, AnimatedRect>,
|
presentations: HashMap<WindowId, AnimatedRect>,
|
||||||
|
opacity: AnimatedValue,
|
||||||
|
strip_offsets: HashMap<OutputId, usize>,
|
||||||
|
font_family: String,
|
||||||
|
font_loaded: bool,
|
||||||
|
font: Option<FontArc>,
|
||||||
|
labels: HashMap<
|
||||||
|
(String, u64),
|
||||||
|
(
|
||||||
|
MemoryRenderBuffer,
|
||||||
|
smithay::utils::Size<i32, smithay::utils::Buffer>,
|
||||||
|
),
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OverviewState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
active: false,
|
||||||
|
selected: None,
|
||||||
|
presentations: HashMap::new(),
|
||||||
|
opacity: AnimatedValue::new(0.0),
|
||||||
|
strip_offsets: HashMap::new(),
|
||||||
|
font_family: "sans-serif".into(),
|
||||||
|
font_loaded: false,
|
||||||
|
font: None,
|
||||||
|
labels: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct WorkspaceCard {
|
||||||
|
pub workspace: WorkspaceId,
|
||||||
|
pub rect: Rect,
|
||||||
|
pub selected: bool,
|
||||||
|
pub windows: Vec<(WindowId, Rect)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn workspace_strip(bounds: Rect) -> Rect {
|
||||||
|
let inset = 24.0_f64.min(bounds.width * 0.05);
|
||||||
|
Rect::new(
|
||||||
|
bounds.x + inset,
|
||||||
|
bounds.y + 16.0,
|
||||||
|
(bounds.width - inset * 2.0).max(1.0),
|
||||||
|
(bounds.height * 0.24).clamp(40.0, 132.0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_area(bounds: Rect) -> Rect {
|
||||||
|
let strip = workspace_strip(bounds);
|
||||||
|
let top = strip.y + strip.height + 16.0;
|
||||||
|
Rect::new(
|
||||||
|
bounds.x,
|
||||||
|
top,
|
||||||
|
bounds.width,
|
||||||
|
(bounds.y + bounds.height - top).max(1.0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains(rect: Rect, point: Point<f64, Logical>) -> bool {
|
||||||
|
point.x >= rect.x
|
||||||
|
&& point.x < rect.x + rect.width
|
||||||
|
&& point.y >= rect.y
|
||||||
|
&& point.y < rect.y + rect.height
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_capacity(strip: Rect) -> usize {
|
||||||
|
(((strip.width - 24.0) + WORKSPACE_CARD_GAP) / (160.0 + WORKSPACE_CARD_GAP))
|
||||||
|
.floor()
|
||||||
|
.max(1.0) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
fn centered_row_start(strip: Rect, count: usize, card_width: f64) -> f64 {
|
||||||
|
let width = count as f64 * card_width + count.saturating_sub(1) as f64 * WORKSPACE_CARD_GAP;
|
||||||
|
strip.x + (strip.width - width) * 0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OverviewState {
|
impl OverviewState {
|
||||||
|
pub(crate) fn with_font_family(font_family: String) -> Self {
|
||||||
|
Self {
|
||||||
|
font_family,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(
|
||||||
|
&mut self,
|
||||||
|
text: &str,
|
||||||
|
scale: f64,
|
||||||
|
) -> Option<(
|
||||||
|
MemoryRenderBuffer,
|
||||||
|
smithay::utils::Size<i32, smithay::utils::Buffer>,
|
||||||
|
)> {
|
||||||
|
let key = (text.to_owned(), scale.to_bits());
|
||||||
|
if let Some(buffer) = self.labels.get(&key) {
|
||||||
|
return Some(buffer.clone());
|
||||||
|
}
|
||||||
|
if !self.font_loaded {
|
||||||
|
self.font_loaded = true;
|
||||||
|
let mut database = fontdb::Database::new();
|
||||||
|
database.load_system_fonts();
|
||||||
|
let families = [
|
||||||
|
fontdb::Family::Name(&self.font_family),
|
||||||
|
fontdb::Family::SansSerif,
|
||||||
|
];
|
||||||
|
self.font = database
|
||||||
|
.query(&fontdb::Query {
|
||||||
|
families: &families,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.and_then(|id| {
|
||||||
|
database.with_face_data(id, |bytes, index| {
|
||||||
|
FontVec::try_from_vec_and_index(bytes.to_vec(), index)
|
||||||
|
.ok()
|
||||||
|
.map(FontArc::new)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
}
|
||||||
|
let font = self.font.as_ref()?;
|
||||||
|
let scaled = font.as_scaled((13.0 * scale) as f32);
|
||||||
|
let width = (text
|
||||||
|
.chars()
|
||||||
|
.map(|ch| scaled.h_advance(font.glyph_id(ch)))
|
||||||
|
.sum::<f32>()
|
||||||
|
.ceil() as i32
|
||||||
|
+ 4)
|
||||||
|
.max(1);
|
||||||
|
let height = (18.0 * scale).ceil() as i32;
|
||||||
|
let mut pixels = vec![0_u8; (width * height * 4) as usize];
|
||||||
|
let mut pen = 2.0;
|
||||||
|
for ch in text.chars() {
|
||||||
|
let id = font.glyph_id(ch);
|
||||||
|
let glyph = id.with_scale_and_position(
|
||||||
|
scaled.scale(),
|
||||||
|
ab_glyph::point(pen, scaled.ascent() + 1.0),
|
||||||
|
);
|
||||||
|
pen += scaled.h_advance(id);
|
||||||
|
if let Some(outline) = font.outline_glyph(glyph) {
|
||||||
|
let bounds = outline.px_bounds();
|
||||||
|
outline.draw(|x, y, coverage| {
|
||||||
|
let x = x as i32 + bounds.min.x as i32;
|
||||||
|
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 buffer = MemoryRenderBuffer::from_slice(
|
||||||
|
&pixels,
|
||||||
|
Fourcc::Abgr8888,
|
||||||
|
(width, height),
|
||||||
|
1,
|
||||||
|
Transform::Normal,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
if self.labels.len() >= 128 {
|
||||||
|
self.labels.clear();
|
||||||
|
}
|
||||||
|
let label = (buffer, (width, height).into());
|
||||||
|
self.labels.insert(key, label.clone());
|
||||||
|
Some(label)
|
||||||
|
}
|
||||||
pub(crate) fn is_active(&self) -> bool {
|
pub(crate) fn is_active(&self) -> bool {
|
||||||
self.active
|
self.active
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_presenting(&self) -> bool {
|
pub(crate) fn is_presenting(&self) -> bool {
|
||||||
self.active || !self.presentations.is_empty()
|
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 selected(&self) -> Option<WindowId> {
|
pub(crate) fn selected(&self) -> Option<WindowId> {
|
||||||
@@ -73,6 +251,11 @@ impl OverviewState {
|
|||||||
animations_enabled: bool,
|
animations_enabled: bool,
|
||||||
) {
|
) {
|
||||||
self.active = true;
|
self.active = true;
|
||||||
|
self.opacity.retarget_preserving_motion(1.0);
|
||||||
|
if !animations_enabled {
|
||||||
|
self.opacity.snap();
|
||||||
|
}
|
||||||
|
self.strip_offsets.clear();
|
||||||
self.selected = selected
|
self.selected = selected
|
||||||
.filter(|id| targets.contains_key(id))
|
.filter(|id| targets.contains_key(id))
|
||||||
.or_else(|| targets.keys().copied().min_by_key(|id| id.0));
|
.or_else(|| targets.keys().copied().min_by_key(|id| id.0));
|
||||||
@@ -93,10 +276,20 @@ impl OverviewState {
|
|||||||
presentation.snap();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit(&mut self, normal: HashMap<WindowId, Rect>, animations_enabled: bool) {
|
fn exit(&mut self, normal: HashMap<WindowId, Rect>, animations_enabled: bool) {
|
||||||
self.active = false;
|
self.active = false;
|
||||||
|
self.opacity.retarget_preserving_motion(0.0);
|
||||||
|
if !animations_enabled {
|
||||||
|
self.opacity.snap();
|
||||||
|
}
|
||||||
self.selected = None;
|
self.selected = None;
|
||||||
self.presentations.retain(|id, presentation| {
|
self.presentations.retain(|id, presentation| {
|
||||||
let Some(target) = normal.get(id).copied() else {
|
let Some(target) = normal.get(id).copied() else {
|
||||||
@@ -135,15 +328,43 @@ impl OverviewState {
|
|||||||
self.presentations.clear();
|
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
|
active_animation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ferese {
|
impl Ferese {
|
||||||
|
pub(crate) fn overview_workspace_label(
|
||||||
|
&mut self,
|
||||||
|
workspace: WorkspaceId,
|
||||||
|
scale: f64,
|
||||||
|
) -> Option<(
|
||||||
|
MemoryRenderBuffer,
|
||||||
|
smithay::utils::Size<i32, smithay::utils::Buffer>,
|
||||||
|
)> {
|
||||||
|
let name = self.workspaces.workspace(workspace)?.name.clone();
|
||||||
|
self.overview.label(&name, scale)
|
||||||
|
}
|
||||||
pub(crate) fn set_overview_active(&mut self, active: bool) {
|
pub(crate) fn set_overview_active(&mut self, active: bool) {
|
||||||
if self.overview.is_active() == active {
|
if self.overview.is_active() == active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Account for idle time before starting a new transition, not after.
|
||||||
|
self.advance_animations(std::time::Instant::now());
|
||||||
|
|
||||||
if active {
|
if active {
|
||||||
let targets = self.overview_targets();
|
let targets = self.overview_targets();
|
||||||
@@ -233,6 +454,155 @@ impl Ferese {
|
|||||||
self.overview.is_active() && self.overview.selected() == Some(id)
|
self.overview.is_active() && self.overview.selected() == Some(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn overview_workspace_cards(&self, output: &Output) -> Vec<WorkspaceCard> {
|
||||||
|
let Some(bounds) = self.output_bounds_for(output) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let Some(output_id) = self.output_id(output) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let selected = self.output_workspaces.active_workspace(output_id);
|
||||||
|
let strip = workspace_strip(bounds);
|
||||||
|
let capacity = strip_capacity(strip);
|
||||||
|
let mut workspaces = self.workspaces.iter().collect::<Vec<_>>();
|
||||||
|
workspaces.sort_by_key(|workspace| workspace.name.parse::<u64>().unwrap_or(workspace.id.0));
|
||||||
|
let selected_index = workspaces
|
||||||
|
.iter()
|
||||||
|
.position(|workspace| Some(workspace.id) == selected)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let max_offset = workspaces.len().saturating_sub(capacity);
|
||||||
|
let offset = self
|
||||||
|
.overview
|
||||||
|
.strip_offsets
|
||||||
|
.get(&output_id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| selected_index.saturating_sub(capacity - 1))
|
||||||
|
.min(max_offset);
|
||||||
|
let card_width =
|
||||||
|
((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64)
|
||||||
|
/ capacity as f64)
|
||||||
|
.min(160.0)
|
||||||
|
.max(1.0);
|
||||||
|
let visible_count = workspaces.len().saturating_sub(offset).min(capacity);
|
||||||
|
let start_x = centered_row_start(strip, visible_count, card_width);
|
||||||
|
workspaces
|
||||||
|
.into_iter()
|
||||||
|
.skip(offset)
|
||||||
|
.take(capacity)
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, workspace)| {
|
||||||
|
let rect = Rect::new(
|
||||||
|
start_x + index as f64 * (card_width + WORKSPACE_CARD_GAP),
|
||||||
|
strip.y + 12.0,
|
||||||
|
card_width,
|
||||||
|
(strip.height - 24.0).max(1.0),
|
||||||
|
);
|
||||||
|
let windows = workspace
|
||||||
|
.layout
|
||||||
|
.window_ids()
|
||||||
|
.chain(workspace.floating.iter().copied())
|
||||||
|
.take(4)
|
||||||
|
.filter_map(|id| {
|
||||||
|
let source = self.window_geometry.get(&id)?.visual.current;
|
||||||
|
Some((id, source))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let preview_bounds = Rect::new(
|
||||||
|
rect.x + 6.0,
|
||||||
|
rect.y + 6.0,
|
||||||
|
(rect.width - 12.0).max(1.0),
|
||||||
|
(rect.height - 26.0).max(1.0),
|
||||||
|
);
|
||||||
|
let windows = preview_layout(preview_bounds, &windows, 4.0, 4.0)
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
WorkspaceCard {
|
||||||
|
workspace: workspace.id,
|
||||||
|
rect,
|
||||||
|
selected: Some(workspace.id) == selected,
|
||||||
|
windows,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn overview_strip_at(&self, point: Point<f64, Logical>) -> Option<Output> {
|
||||||
|
if !self.overview.is_active() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.space
|
||||||
|
.outputs()
|
||||||
|
.find(|output| {
|
||||||
|
self.output_bounds_for(output)
|
||||||
|
.is_some_and(|bounds| contains(workspace_strip(bounds), point))
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn click_overview_workspace(&mut self, point: Point<f64, Logical>) -> bool {
|
||||||
|
let Some(output) = self.overview_strip_at(point) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let card = self
|
||||||
|
.overview_workspace_cards(&output)
|
||||||
|
.into_iter()
|
||||||
|
.find(|card| contains(card.rect, point));
|
||||||
|
if let Some(card) = card {
|
||||||
|
let Some(output_id) = self.output_id(&output) else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if self
|
||||||
|
.output_workspaces
|
||||||
|
.switch_workspace(output_id, card.workspace)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
// Existing monitor ownership wins; never steal another output's workspace.
|
||||||
|
self.restore_output_focus();
|
||||||
|
self.overview.strip_offsets.clear();
|
||||||
|
self.relayout();
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::backends::direct::render_all(self);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn scroll_overview_strip(&mut self, point: Point<f64, Logical>, delta: f64) -> bool {
|
||||||
|
let Some(output) = self.overview_strip_at(point) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(output_id) = self.output_id(&output) else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let Some(bounds) = self.output_bounds_for(&output) else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let capacity = strip_capacity(workspace_strip(bounds));
|
||||||
|
let cards = self.overview_workspace_cards(&output);
|
||||||
|
let mut ordered = self.workspaces.iter().collect::<Vec<_>>();
|
||||||
|
ordered.sort_by_key(|workspace| workspace.name.parse::<u64>().unwrap_or(workspace.id.0));
|
||||||
|
let current = cards
|
||||||
|
.first()
|
||||||
|
.and_then(|card| {
|
||||||
|
ordered
|
||||||
|
.iter()
|
||||||
|
.position(|workspace| workspace.id == card.workspace)
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
let next = if delta > 0.0 {
|
||||||
|
current.saturating_add(1)
|
||||||
|
} else if delta < 0.0 {
|
||||||
|
current.saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
current
|
||||||
|
};
|
||||||
|
self.overview
|
||||||
|
.strip_offsets
|
||||||
|
.insert(output_id, next.min(ordered.len().saturating_sub(capacity)));
|
||||||
|
crate::backends::direct::render_all(self);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn overview_targets(&self) -> HashMap<WindowId, (Rect, Rect)> {
|
fn overview_targets(&self) -> HashMap<WindowId, (Rect, Rect)> {
|
||||||
let mut targets = HashMap::new();
|
let mut targets = HashMap::new();
|
||||||
|
|
||||||
@@ -257,7 +627,7 @@ impl Ferese {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
windows.sort_by_key(|(id, _)| id.0);
|
windows.sort_by_key(|(id, _)| id.0);
|
||||||
for (id, target) in overview_layout(bounds, &windows) {
|
for (id, target) in overview_layout(window_area(bounds), &windows) {
|
||||||
let normal = self.window_geometry[&id].visual.current;
|
let normal = self.window_geometry[&id].visual.current;
|
||||||
targets.insert(id, (normal, target));
|
targets.insert(id, (normal, target));
|
||||||
}
|
}
|
||||||
@@ -275,22 +645,31 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn overview_layout(bounds: Rect, windows: &[(WindowId, Rect)]) -> HashMap<WindowId, Rect> {
|
fn overview_layout(bounds: Rect, windows: &[(WindowId, Rect)]) -> HashMap<WindowId, Rect> {
|
||||||
|
preview_layout(bounds, windows, OVERVIEW_MARGIN, OVERVIEW_GAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview_layout(
|
||||||
|
bounds: Rect,
|
||||||
|
windows: &[(WindowId, Rect)],
|
||||||
|
margin: f64,
|
||||||
|
gap: f64,
|
||||||
|
) -> HashMap<WindowId, Rect> {
|
||||||
if windows.is_empty() {
|
if windows.is_empty() {
|
||||||
return HashMap::new();
|
return HashMap::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let content_width = (bounds.width - OVERVIEW_MARGIN * 2.0).max(1.0);
|
let margin = margin.min(bounds.width * 0.1).min(bounds.height * 0.1);
|
||||||
let content_height = (bounds.height - OVERVIEW_MARGIN * 2.0).max(1.0);
|
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 aspect = (content_width / content_height).max(0.1);
|
||||||
let columns = ((windows.len() as f64 * aspect).sqrt().ceil() as usize).clamp(1, windows.len());
|
let columns = ((windows.len() as f64 * aspect).sqrt().ceil() as usize).clamp(1, windows.len());
|
||||||
let rows = windows.len().div_ceil(columns);
|
let rows = windows.len().div_ceil(columns);
|
||||||
let cell_width = ((content_width - OVERVIEW_GAP * columns.saturating_sub(1) as f64)
|
let cell_width =
|
||||||
/ columns as f64)
|
((content_width - gap * columns.saturating_sub(1) as f64) / columns as f64).max(1.0);
|
||||||
.max(1.0);
|
|
||||||
let cell_height =
|
let cell_height =
|
||||||
((content_height - OVERVIEW_GAP * rows.saturating_sub(1) as f64) / rows as f64).max(1.0);
|
((content_height - gap * rows.saturating_sub(1) as f64) / rows as f64).max(1.0);
|
||||||
let origin_x = bounds.x + OVERVIEW_MARGIN;
|
let origin_x = bounds.x + margin;
|
||||||
let origin_y = bounds.y + OVERVIEW_MARGIN;
|
let origin_y = bounds.y + margin;
|
||||||
|
|
||||||
windows
|
windows
|
||||||
.iter()
|
.iter()
|
||||||
@@ -303,8 +682,8 @@ fn overview_layout(bounds: Rect, windows: &[(WindowId, Rect)]) -> HashMap<Window
|
|||||||
.min(MAX_PREVIEW_SCALE);
|
.min(MAX_PREVIEW_SCALE);
|
||||||
let width = (source.width * scale).max(1.0);
|
let width = (source.width * scale).max(1.0);
|
||||||
let height = (source.height * 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_x = origin_x + column as f64 * (cell_width + gap);
|
||||||
let cell_y = origin_y + row as f64 * (cell_height + OVERVIEW_GAP);
|
let cell_y = origin_y + row as f64 * (cell_height + gap);
|
||||||
let target = Rect::new(
|
let target = Rect::new(
|
||||||
cell_x + (cell_width - width) / 2.0,
|
cell_x + (cell_width - width) / 2.0,
|
||||||
cell_y + (cell_height - height) / 2.0,
|
cell_y + (cell_height - height) / 2.0,
|
||||||
@@ -343,6 +722,99 @@ fn directional_distance(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_cards_are_centered_as_a_group_on_each_output() {
|
||||||
|
for origin in [0.0, 1600.0] {
|
||||||
|
let strip = Rect::new(origin + 24.0, 64.0, 1552.0, 132.0);
|
||||||
|
for count in [1, 3, 8] {
|
||||||
|
let start = centered_row_start(strip, count, 160.0);
|
||||||
|
let end = start
|
||||||
|
+ count as f64 * 160.0
|
||||||
|
+ count.saturating_sub(1) as f64 * WORKSPACE_CARD_GAP;
|
||||||
|
assert!((start - strip.x - (strip.x + strip.width - end)).abs() < 0.001);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_strip_is_tinted_and_windows_start_below_it() {
|
||||||
|
for bounds in [
|
||||||
|
Rect::new(0.0, 48.0, 1600.0, 952.0),
|
||||||
|
Rect::new(1600.0, 48.0, 2400.0, 1302.0),
|
||||||
|
] {
|
||||||
|
let strip = workspace_strip(bounds);
|
||||||
|
let area = window_area(bounds);
|
||||||
|
assert!(strip.height < bounds.height / 2.0);
|
||||||
|
assert!(area.y > strip.y + strip.height);
|
||||||
|
let windows = [(WindowId(1), Rect::new(0.0, 0.0, 800.0, 900.0))];
|
||||||
|
assert!(
|
||||||
|
overview_layout(area, &windows)
|
||||||
|
.values()
|
||||||
|
.all(|rect| rect.y >= area.y)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_workspace_fades_without_an_abrupt_last_ten_percent() {
|
||||||
|
let mut overview = OverviewState::default();
|
||||||
|
overview.enter(HashMap::new(), None, false);
|
||||||
|
overview.exit(HashMap::new(), true);
|
||||||
|
assert!(overview.is_presenting());
|
||||||
|
assert_eq!(overview.opacity(), 1.0);
|
||||||
|
let mut previous = 1.0;
|
||||||
|
for _ in 0..100 {
|
||||||
|
overview.advance(Duration::from_millis(16), SpringConfig::default(), true);
|
||||||
|
let current = overview.opacity();
|
||||||
|
assert!(current <= previous);
|
||||||
|
if current == 0.0 {
|
||||||
|
assert!(previous < 0.01);
|
||||||
|
}
|
||||||
|
previous = current;
|
||||||
|
}
|
||||||
|
assert_eq!(overview.opacity(), 0.0);
|
||||||
|
assert!(!overview.is_presenting());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reversing_overview_preserves_current_opacity() {
|
||||||
|
let mut overview = OverviewState::default();
|
||||||
|
overview.enter(HashMap::new(), None, true);
|
||||||
|
overview.advance(Duration::from_millis(48), SpringConfig::default(), true);
|
||||||
|
let opacity = overview.opacity();
|
||||||
|
assert!(opacity > 0.0 && opacity < 1.0);
|
||||||
|
overview.exit(HashMap::new(), true);
|
||||||
|
assert_eq!(overview.opacity(), opacity);
|
||||||
|
overview.advance(Duration::from_millis(16), SpringConfig::default(), true);
|
||||||
|
let opacity = overview.opacity();
|
||||||
|
overview.enter(HashMap::new(), None, true);
|
||||||
|
assert_eq!(overview.opacity(), opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reduced_motion_snaps_overview_chrome() {
|
||||||
|
let mut overview = OverviewState::default();
|
||||||
|
overview.enter(HashMap::new(), None, false);
|
||||||
|
assert_eq!(overview.opacity(), 1.0);
|
||||||
|
overview.exit(HashMap::new(), false);
|
||||||
|
assert_eq!(overview.opacity(), 0.0);
|
||||||
|
assert!(!overview.is_presenting());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn thumbnail_grid_preserves_aspect_and_does_not_overlap_labels() {
|
||||||
|
let bounds = Rect::new(30.0, 50.0, 148.0, 76.0);
|
||||||
|
let windows = (1..=4)
|
||||||
|
.map(|id| (WindowId(id), Rect::new(0.0, 0.0, 1000.0, 800.0)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let layout = preview_layout(bounds, &windows, 4.0, 4.0);
|
||||||
|
for rect in layout.values() {
|
||||||
|
assert!((rect.width / rect.height - 1.25).abs() < 0.001);
|
||||||
|
assert!(rect.y + rect.height <= bounds.y + bounds.height);
|
||||||
|
assert!(rect.x + rect.width <= bounds.x + bounds.width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn overview_grid_fits_windows_inside_the_output() {
|
fn overview_grid_fits_windows_inside_the_output() {
|
||||||
let bounds = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
let bounds = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ pub struct Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct RuntimeConfig {
|
pub struct RuntimeConfig {
|
||||||
|
pub(crate) overview_font_family: String,
|
||||||
pub(crate) wallpaper: crate::wallpaper::WallpaperConfig,
|
pub(crate) wallpaper: crate::wallpaper::WallpaperConfig,
|
||||||
pub layout_mode: LayoutMode,
|
pub layout_mode: LayoutMode,
|
||||||
pub gap_config: GapConfig,
|
pub gap_config: GapConfig,
|
||||||
@@ -519,7 +520,7 @@ impl Ferese {
|
|||||||
pending_screencopies: Vec::new(),
|
pending_screencopies: Vec::new(),
|
||||||
shell_resources: Vec::new(),
|
shell_resources: Vec::new(),
|
||||||
shell_snapshot_serial: 0,
|
shell_snapshot_serial: 0,
|
||||||
overview: crate::overview::OverviewState::default(),
|
overview: crate::overview::OverviewState::with_font_family(config.overview_font_family),
|
||||||
next_window_id: 1,
|
next_window_id: 1,
|
||||||
next_output_id: 1,
|
next_output_id: 1,
|
||||||
last_animation_tick: start_time,
|
last_animation_tick: start_time,
|
||||||
@@ -693,7 +694,12 @@ impl Ferese {
|
|||||||
|
|
||||||
pub fn window_under_visual(&self, position: Point<f64, Logical>) -> Option<Window> {
|
pub fn window_under_visual(&self, position: Point<f64, Logical>) -> Option<Window> {
|
||||||
let workspace = self.workspace_under_pointer(position)?;
|
let workspace = self.workspace_under_pointer(position)?;
|
||||||
self.space.elements().rev().find_map(|window| {
|
let candidates = if self.overview.is_active() {
|
||||||
|
self.window_ids.keys().cloned().collect::<Vec<_>>()
|
||||||
|
} else {
|
||||||
|
self.space.elements().rev().cloned().collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
candidates.iter().find_map(|window| {
|
||||||
let id = self.window_ids.get(window)?;
|
let id = self.window_ids.get(window)?;
|
||||||
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
||||||
return None;
|
return None;
|
||||||
|
|||||||
+226
-33
@@ -20,7 +20,7 @@ use smithay::{
|
|||||||
Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage,
|
Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage,
|
||||||
memory::MemoryRenderBufferRenderElement,
|
memory::MemoryRenderBufferRenderElement,
|
||||||
render_elements,
|
render_elements,
|
||||||
solid::{SolidColorBuffer, SolidColorRenderElement},
|
solid::SolidColorRenderElement,
|
||||||
surface::{
|
surface::{
|
||||||
WaylandSurfaceRenderElement, WaylandSurfaceTexture,
|
WaylandSurfaceRenderElement, WaylandSurfaceTexture,
|
||||||
render_elements_from_surface_tree,
|
render_elements_from_surface_tree,
|
||||||
@@ -554,15 +554,7 @@ pub(crate) struct RoundedClipPrograms {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct OverviewScrim {
|
pub(crate) struct OverviewScrim {
|
||||||
buffer: SolidColorBuffer,
|
chrome: HashMap<(u64, bool), HashMap<ErasedContextId, CachedBorder>>,
|
||||||
}
|
|
||||||
|
|
||||||
impl OverviewScrim {
|
|
||||||
fn new(size: Size<i32, Logical>) -> Self {
|
|
||||||
Self {
|
|
||||||
buffer: SolidColorBuffer::new(size, [0.02, 0.025, 0.035, 0.62]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -1013,10 +1005,27 @@ fn output_elements(
|
|||||||
&[Layer::Overlay, Layer::Top]
|
&[Layer::Overlay, Layer::Top]
|
||||||
};
|
};
|
||||||
elements.extend(layer_elements(state, renderer, output, upper_layers));
|
elements.extend(layer_elements(state, renderer, output, upper_layers));
|
||||||
let windows = state
|
if state.overview.is_presenting() {
|
||||||
.space
|
elements.extend(overview_strip_elements(
|
||||||
.elements()
|
state,
|
||||||
.rev()
|
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| {
|
.filter_map(|window| {
|
||||||
let id = *state.window_ids.get(window)?;
|
let id = *state.window_ids.get(window)?;
|
||||||
// Scrolling columns may sit outside their monitor's rectangle.
|
// 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(
|
elements.extend(layer_elements(
|
||||||
state,
|
state,
|
||||||
renderer,
|
renderer,
|
||||||
@@ -1236,29 +1240,218 @@ fn output_elements(
|
|||||||
elements
|
elements
|
||||||
}
|
}
|
||||||
|
|
||||||
fn overview_scrim_element(
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn overview_chrome_element(
|
||||||
state: &mut Ferese,
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
output_geometry: Rectangle<i32, Logical>,
|
output_geometry: Rectangle<i32, Logical>,
|
||||||
scale: f64,
|
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 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
|
.overview_scrims
|
||||||
.entry(output_id)
|
.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
|
fn overview_strip_elements(
|
||||||
.buffer
|
state: &mut Ferese,
|
||||||
.update(output_geometry.size, [0.02, 0.025, 0.035, 0.62]);
|
renderer: &mut GlesRenderer,
|
||||||
|
output: &Output,
|
||||||
Some(SolidColorRenderElement::from_buffer(
|
output_geometry: Rectangle<i32, Logical>,
|
||||||
&scrim.buffer,
|
scale: f64,
|
||||||
Point::<i32, Physical>::default(),
|
) -> 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,
|
scale,
|
||||||
1.0,
|
(0, false),
|
||||||
RenderElementKind::Unspecified,
|
crate::overview::workspace_strip(bounds),
|
||||||
))
|
color,
|
||||||
|
0.0,
|
||||||
|
16.0,
|
||||||
|
) {
|
||||||
|
elements.push(strip.into());
|
||||||
|
}
|
||||||
|
elements
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rounded_clip_program(
|
fn rounded_clip_program(
|
||||||
|
|||||||
+12
-3
@@ -48,8 +48,8 @@ fn parse_args(args: impl IntoIterator<Item = String>) -> Result<(String, Value),
|
|||||||
json!({ "index": index })
|
json!({ "index": index })
|
||||||
}
|
}
|
||||||
"toggle-floating" | "toggle-fullscreen" | "toggle-maximized" | "toggle-layout"
|
"toggle-floating" | "toggle-fullscreen" | "toggle-maximized" | "toggle-layout"
|
||||||
| "cycle-column-width" | "center-column" | "consume" | "expel" | "close"
|
| "toggle-overview" | "cycle-column-width" | "center-column" | "consume" | "expel"
|
||||||
| "get-focused-window" | "get-workspaces" | "get-outputs" | "exit" => {
|
| "close" | "get-focused-window" | "get-workspaces" | "get-outputs" | "exit" => {
|
||||||
if !positional.is_empty() {
|
if !positional.is_empty() {
|
||||||
return Err(format!("{command} does not accept arguments"));
|
return Err(format!("{command} does not accept arguments"));
|
||||||
}
|
}
|
||||||
@@ -78,13 +78,22 @@ fn socket_path() -> Result<PathBuf, io::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn usage() -> String {
|
fn usage() -> String {
|
||||||
"usage: feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl <toggle-floating|toggle-maximized|toggle-fullscreen|toggle-layout>\n feresectl <cycle-column-width|center-column|consume|expel|close|exit>\n feresectl <get-focused-window|get-workspaces|get-outputs>".to_owned()
|
"usage: feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl <toggle-floating|toggle-maximized|toggle-fullscreen|toggle-layout|toggle-overview>\n feresectl <cycle-column-width|center-column|consume|expel|close|exit>\n feresectl <get-focused-window|get-workspaces|get-outputs>".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overview_toggle_accepts_no_arguments() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(["toggle-overview".into()]).unwrap(),
|
||||||
|
("toggle-overview".into(), json!({}))
|
||||||
|
);
|
||||||
|
assert!(parse_args(["toggle-overview".into(), "extra".into()]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_direction_and_workspace_commands() {
|
fn parses_direction_and_workspace_commands() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
Reference in new issue
Block a user