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