micro optimization fixes

This commit is contained in:
Samuel Onoja committed 2026-10-01 20:47:05 +01:00
1 parent 0d7203f84b
commit da60ecb719
36 files changed
+1707 -447

No files matched your search

+14 -6
View File
@@ -773,7 +773,13 @@ fn lid_hides_panel(closed: bool, external_available: bool) -> bool {
closed && external_available closed && external_available
} }
fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: crate::frame_scheduler::FramePlan) { // Return the sampled scene's activity so dispatch does not rescan the windows.
fn render_output(
state: &mut Ferese,
node: DrmNode,
crtc: crtc::Handle,
plan: crate::frame_scheduler::FramePlan,
) -> bool {
if state.session_lock.active && state.session_lock.sleeping { if state.session_lock.active && state.session_lock.sleeping {
sleep_locked_outputs(state); sleep_locked_outputs(state);
let asleep = state let asleep = state
@@ -783,7 +789,7 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr
.and_then(|device| device.outputs.get(&crtc)) .and_then(|device| device.outputs.get(&crtc))
.is_none_or(|output| output.power_off || output.frame_pending); .is_none_or(|output| output.power_off || output.frame_pending);
if asleep { if asleep {
return; return false;
} }
} }
@@ -798,18 +804,18 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr
.as_mut() .as_mut()
.and_then(|backend| backend.devices.remove(&node)) .and_then(|backend| backend.devices.remove(&node))
else { else {
return; return false;
}; };
let Some(mut output) = device.outputs.remove(&crtc) else { let Some(mut output) = device.outputs.remove(&crtc) else {
restore_device(state, node, device); restore_device(state, node, device);
return; return false;
}; };
if !device.drm.is_active() || output.frame_pending { if !device.drm.is_active() || output.frame_pending {
device.outputs.insert(crtc, output); device.outputs.insert(crtc, output);
restore_device(state, node, device); restore_device(state, node, device);
return; return false;
} }
let render_started = Instant::now(); let render_started = Instant::now();
@@ -826,7 +832,7 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale()); let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
let flags = planes::frame_flags( let flags = planes::frame_flags(
state.output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting()), state.output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting()),
state.output_has_animations(&output.output), frame.animating,
state.session_lock.active, state.session_lock.active,
); );
let result = let result =
@@ -919,6 +925,8 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr
if !submitted { if !submitted {
schedule_frame_callbacks(state, node, crtc, plan.present_at); schedule_frame_callbacks(state, node, crtc, plan.present_at);
} }
frame.animating
} }
fn restore_device(state: &mut Ferese, node: DrmNode, device: DirectDevice) { fn restore_device(state: &mut Ferese, node: DrmNode, device: DirectDevice) {
+3 -2
View File
@@ -120,8 +120,9 @@ fn dispatch_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identit
backend.animation_active = animating; backend.animation_active = animating;
} }
render_output(state, node, crtc, plan); let output_animating = render_output(state, node, crtc, plan);
if state.output_has_animations(identity) {
if output_animating {
request_frame(state, node, crtc); request_frame(state, node, crtc);
} }
+7 -5
View File
@@ -11,9 +11,9 @@ use std::path::PathBuf;
use appearance::*; use appearance::*;
pub use appearance::{BorderGradient, InactiveDimSettings, MaterialStyle, ThemeSettings}; pub use appearance::{BorderGradient, InactiveDimSettings, MaterialStyle, ThemeSettings};
pub(crate) use bindings::physical_keymap;
use bindings::*; use bindings::*;
pub use bindings::{Binding, BindingAction}; pub use bindings::{Binding, BindingAction};
pub(crate) use bindings::{BindingSet, physical_keymap};
use ferese_animation::SpringConfig; use ferese_animation::SpringConfig;
use ferese_core::LayoutMode; use ferese_core::LayoutMode;
use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy}; use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy};
@@ -939,18 +939,19 @@ mod tests {
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()].into())
})); }));
assert!(bindings.iter().any(|binding| { assert!(bindings.iter().any(|binding| {
binding.modifiers.logo binding.modifiers.logo
&& binding.modifiers.shift && binding.modifiers.shift
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_s) && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_s)
&& binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned()]) && binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned()].into())
})); }));
assert!(bindings.iter().any(|binding| { assert!(bindings.iter().any(|binding| {
binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Print) binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Print)
&& binding.modifiers == BindingModifiers::default() && binding.modifiers == BindingModifiers::default()
&& binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned(), "--full".to_owned()]) && binding.action
== BindingAction::Spawn(vec!["ferese-screenshot".to_owned(), "--full".to_owned()].into())
})); }));
assert!(bindings.iter().any(|binding| { assert!(bindings.iter().any(|binding| {
binding.modifiers.logo binding.modifiers.logo
@@ -970,7 +971,8 @@ mod tests {
let bindings = replaced.bindings(&input).unwrap(); let bindings = replaced.bindings(&input).unwrap();
assert_eq!(bindings.len(), 53); assert_eq!(bindings.len(), 53);
assert!(bindings.iter().any(|binding| { assert!(bindings.iter().any(|binding| {
binding.action == BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()]) binding.action
== BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()].into())
})); }));
let input = unbound.input_settings().unwrap(); let input = unbound.input_settings().unwrap();
+7 -2
View File
@@ -1,5 +1,10 @@
use std::sync::Arc;
use super::*; use super::*;
mod index;
pub(crate) use index::BindingSet;
impl Config { impl Config {
pub fn bindings(&self, input: &InputSettings) -> Result<Vec<Binding>, ConfigError> { pub fn bindings(&self, input: &InputSettings) -> Result<Vec<Binding>, ConfigError> {
let mut commands = HashMap::from([ let mut commands = HashMap::from([
@@ -280,7 +285,7 @@ pub(super) fn parse_action(
let argv = commands let argv = commands
.get(command) .get(command)
.ok_or_else(|| ConfigError::Binding(format!("unknown command {command:?}")))?; .ok_or_else(|| ConfigError::Binding(format!("unknown command {command:?}")))?;
Ok(BindingAction::Spawn(argv.clone())) Ok(BindingAction::Spawn(argv.clone().into()))
} }
"close" => { "close" => {
no_argument()?; no_argument()?;
@@ -440,7 +445,7 @@ pub(super) struct BindingModifiers {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum BindingAction { pub enum BindingAction {
None, None,
Spawn(Vec<String>), Spawn(Arc<[String]>),
Close, Close,
Exit, Exit,
Focus(ferese_layout::Direction), Focus(ferese_layout::Direction),
+179
View File
@@ -0,0 +1,179 @@
use super::*;
/// Immutable bindings and their keyboard index are replaced together on reload.
/// The ordered slice remains available for gestures and the shortcut guide.
pub(crate) struct BindingSet {
ordered: Vec<Binding>,
keyboard: HashMap<BindingIdentity, usize>,
}
impl From<Vec<Binding>> for BindingSet {
fn from(ordered: Vec<Binding>) -> Self {
let mut keyboard = HashMap::with_capacity(ordered.len());
for (index, binding) in ordered.iter().enumerate() {
let identity = binding_identity_for_runtime(binding);
if identity.match_mode != BindingMatch::Swipe {
keyboard.entry(identity).or_insert(index);
}
}
Self { ordered, keyboard }
}
}
impl std::ops::Deref for BindingSet {
type Target = [Binding];
fn deref(&self) -> &Self::Target {
&self.ordered
}
}
impl BindingSet {
pub(crate) fn action_for_key(
&self,
keycode: Keycode,
symbols: &[u32],
logo: bool,
ctrl: bool,
alt: bool,
shift: bool,
) -> Option<&BindingAction> {
let modifiers = BindingModifiers { logo, ctrl, alt, shift };
let physical = self.keyboard.get(&BindingIdentity {
modifiers,
match_mode: BindingMatch::Physical,
key: keycode.raw(),
});
let symbolic = symbols.iter().filter_map(|&key| {
self.keyboard.get(&BindingIdentity {
modifiers,
match_mode: BindingMatch::Keysym,
key,
})
});
// Preserve list precedence across physical keys and all raw symbols;
// neither hash iteration order nor symbol order determines the winner.
physical
.into_iter()
.chain(symbolic)
.min()
.map(|&index| &self.ordered[index].action)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn binding_for(trigger: BindingTrigger, action: BindingAction) -> Binding {
Binding {
modifiers: BindingModifiers::default(),
trigger,
action,
}
}
#[test]
fn keyboard_index_preserves_order_across_symbols_and_physical_keys() {
let code = Keycode::new(38);
let bindings = vec![
binding_for(BindingTrigger::Keysym(keysyms::KEY_b), BindingAction::Close),
binding_for(BindingTrigger::Physical(code), BindingAction::ToggleFloating),
binding_for(BindingTrigger::Keysym(keysyms::KEY_a), BindingAction::Exit),
binding_for(BindingTrigger::Keysym(keysyms::KEY_b), BindingAction::None),
];
for ordered in [bindings.clone(), bindings.into_iter().rev().collect()] {
let index = BindingSet::from(ordered);
for symbols in [
vec![keysyms::KEY_a, keysyms::KEY_b],
vec![keysyms::KEY_b, keysyms::KEY_a],
vec![],
] {
let expected = index
.iter()
.find(|binding| binding.matches(code, &symbols, false, false, false, false));
assert_eq!(
index.action_for_key(code, &symbols, false, false, false, false),
expected.map(|binding| &binding.action)
);
}
}
}
#[test]
fn indexed_lookup_matches_linear_lookup_for_all_modifier_combinations() {
let config = Config::default();
let mut bindings = config.bindings(&config.input_settings().unwrap()).unwrap();
bindings.extend([
binding_for(BindingTrigger::Physical(Keycode::new(38)), BindingAction::None),
binding_for(BindingTrigger::Keysym(keysyms::KEY_a), BindingAction::Close),
binding_for(
BindingTrigger::Swipe {
fingers: 3,
direction: crate::gestures::SwipeDirection::Left,
},
BindingAction::Focus(Direction::Left),
),
]);
let index = BindingSet::from(bindings);
for bits in 0..16 {
let (logo, ctrl, alt, shift) = (bits & 1 != 0, bits & 2 != 0, bits & 4 != 0, bits & 8 != 0);
for code in 8..100 {
for symbols in [
vec![],
vec![keysyms::KEY_Return],
vec![keysyms::KEY_b, keysyms::KEY_a],
vec![keysyms::KEY_a, keysyms::KEY_Tab],
vec![keysyms::KEY_NoSymbol],
] {
let code = Keycode::new(code);
let expected = index
.iter()
.find(|binding| binding.matches(code, &symbols, logo, ctrl, alt, shift));
assert_eq!(
index.action_for_key(code, &symbols, logo, ctrl, alt, shift),
expected.map(|binding| &binding.action)
);
}
}
}
}
#[test]
fn replacing_bindings_discards_old_index_entries() {
let code = Keycode::new(38);
let mut bindings = BindingSet::from(vec![binding_for(BindingTrigger::Physical(code), BindingAction::Close)]);
assert_eq!(
bindings.action_for_key(code, &[], false, false, false, false),
Some(&BindingAction::Close)
);
bindings = BindingSet::from(vec![binding_for(
BindingTrigger::Keysym(keysyms::KEY_b),
BindingAction::None,
)]);
assert_eq!(bindings.action_for_key(code, &[], false, false, false, false), None);
assert_eq!(
bindings.action_for_key(code, &[keysyms::KEY_b], false, false, false, false),
Some(&BindingAction::None)
);
}
#[test]
fn cloning_spawn_actions_shares_the_command_storage() {
let action = BindingAction::Spawn(vec!["foot".into(), "--app-id".into(), "work".into()].into());
let cloned = action.clone();
let (BindingAction::Spawn(original), BindingAction::Spawn(cloned)) = (&action, cloned) else {
unreachable!();
};
assert!(Arc::ptr_eq(original, &cloned));
assert_eq!(original[0], "foot");
assert_eq!(original.len(), 3);
}
}
+4
View File
@@ -27,6 +27,10 @@ pub(crate) struct DimAnimation {
} }
impl DimAnimation { impl DimAnimation {
pub(crate) fn needs_update(&self, target: f64) -> bool {
self.target != target || self.current != target
}
pub(crate) fn is_animating(&self) -> bool { pub(crate) fn is_animating(&self) -> bool {
self.current != self.target self.current != self.target
} }
+1
View File
@@ -84,6 +84,7 @@ impl CompositorHandler for Ferese {
} else if self.windows.ids().contains_key(&window) { } else if self.windows.ids().contains_key(&window) {
self.remove_tiled_window(&window); self.remove_tiled_window(&window);
self.space.map_element(window.clone(), (0, 0), false); self.space.map_element(window.clone(), (0, 0), false);
self.invalidate_window_stacking();
self.restore_keyboard_focus(); self.restore_keyboard_focus();
} }
if window if window
+2 -14
View File
@@ -59,14 +59,7 @@ impl SeatHandler for Ferese {
} }
fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) { fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {
if let Some(id) = focused.and_then(|surface| { if let Some(id) = focused.and_then(|surface| self.windows.id_for_surface(surface)) {
self.windows.ids().iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface)
.then_some(*id)
})
}) {
self.focus_history.record(id); self.focus_history.record(id);
} }
if let Some(inhibitor) = self.active_shortcuts_inhibitor.take() if let Some(inhibitor) = self.active_shortcuts_inhibitor.take()
@@ -152,12 +145,7 @@ impl Ferese {
{ {
root = parent; root = parent;
} }
let window = self.windows.ids().iter().find_map(|(window, id)| { let window = self.windows.id_for_surface(&root);
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == &root)
.then_some(*id)
});
let output = self.space.outputs().find(|output| { let output = self.space.outputs().find(|output| {
window.is_some_and(|id| self.window_belongs_to_output(id, output)) window.is_some_and(|id| self.window_belongs_to_output(id, output))
|| layer_map_for_output(output) || layer_map_for_output(output)
+34 -45
View File
@@ -30,26 +30,39 @@ impl XdgShellHandler for Ferese {
// discoverable, but do not mutate/focus the layout until its initial // discoverable, but do not mutate/focus the layout until its initial
// commit supplies the metadata needed to resolve placement rules. // commit supplies the metadata needed to resolve placement rules.
self.space.map_element(window, (0, 0), false); self.space.map_element(window, (0, 0), false);
self.invalidate_window_stacking();
}
fn title_changed(&mut self, surface: ToplevelSurface) {
if self.windows.refresh_metadata(surface.wl_surface()) {
self.send_shell_snapshots();
self.cursor_redraw_pending = true;
}
}
fn app_id_changed(&mut self, surface: ToplevelSurface) {
if self.windows.refresh_metadata(surface.wl_surface()) {
self.send_shell_snapshots();
self.cursor_redraw_pending = true;
}
} }
fn parent_changed(&mut self, surface: ToplevelSurface) { fn parent_changed(&mut self, surface: ToplevelSurface) {
let Some(parent) = surface.parent() else { let Some(parent) = surface.parent() else {
return; return;
}; };
let parent = self.windows.ids().iter().find_map(|(window, id)| { let parent = self.windows.id_for_surface(&parent);
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == &parent)
.then_some(*id)
});
let child = self let child = self
.space .windows
.elements() .id_for_surface(surface.wl_surface())
.find(|window| { .and_then(|id| self.windows.window(id))
.or_else(|| {
self.space.elements().find(|window| {
window window
.toplevel() .toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
}) })
})
.cloned(); .cloned();
if let (Some(parent), Some(child)) = (parent, child) { if let (Some(parent), Some(child)) = (parent, child) {
@@ -61,14 +74,15 @@ impl XdgShellHandler for Ferese {
// Hidden workspace windows are managed but unmapped from Space. // Hidden workspace windows are managed but unmapped from Space.
let Some(window) = self let Some(window) = self
.windows .windows
.ids() .id_for_surface(surface.wl_surface())
.keys() .and_then(|id| self.windows.window(id))
.chain(self.space.elements()) .or_else(|| {
.find(|window| { self.space.elements().find(|window| {
window window
.toplevel() .toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
}) })
})
.cloned() .cloned()
else { else {
return; return;
@@ -186,12 +200,7 @@ impl XdgShellHandler for Ferese {
} }
fn maximize_request(&mut self, surface: ToplevelSurface) { fn maximize_request(&mut self, surface: ToplevelSurface) {
if let Some(id) = self.windows.ids().iter().find_map(|(window, id)| { if let Some(id) = self.windows.id_for_surface(surface.wl_surface()) {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
.then_some(*id)
}) {
self.set_window_maximized(id, true); self.set_window_maximized(id, true);
} else { } else {
surface.with_pending_state(|state| state.states.set(ToplevelState::Maximized)); surface.with_pending_state(|state| state.states.set(ToplevelState::Maximized));
@@ -199,12 +208,7 @@ impl XdgShellHandler for Ferese {
} }
fn unmaximize_request(&mut self, surface: ToplevelSurface) { fn unmaximize_request(&mut self, surface: ToplevelSurface) {
if let Some(id) = self.windows.ids().iter().find_map(|(window, id)| { if let Some(id) = self.windows.id_for_surface(surface.wl_surface()) {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
.then_some(*id)
}) {
self.set_window_maximized(id, false); self.set_window_maximized(id, false);
} else { } else {
surface.with_pending_state(|state| state.states.unset(ToplevelState::Maximized)); surface.with_pending_state(|state| state.states.unset(ToplevelState::Maximized));
@@ -216,12 +220,7 @@ impl XdgShellHandler for Ferese {
surface: ToplevelSurface, surface: ToplevelSurface,
_output: Option<smithay::reexports::wayland_server::protocol::wl_output::WlOutput>, _output: Option<smithay::reexports::wayland_server::protocol::wl_output::WlOutput>,
) { ) {
if let Some(window) = self.windows.ids().iter().find_map(|(window, id)| { if let Some(window) = self.windows.id_for_surface(surface.wl_surface()) {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
.then_some(*id)
}) {
self.set_window_fullscreen(window, true); self.set_window_fullscreen(window, true);
} else { } else {
surface.with_pending_state(|state| state.states.set(ToplevelState::Fullscreen)); surface.with_pending_state(|state| state.states.set(ToplevelState::Fullscreen));
@@ -229,12 +228,7 @@ impl XdgShellHandler for Ferese {
} }
fn unfullscreen_request(&mut self, surface: ToplevelSurface) { fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
if let Some(window) = self.windows.ids().iter().find_map(|(window, id)| { if let Some(window) = self.windows.id_for_surface(surface.wl_surface()) {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
.then_some(*id)
}) {
self.set_window_fullscreen(window, false); self.set_window_fullscreen(window, false);
} else { } else {
surface.with_pending_state(|state| state.states.unset(ToplevelState::Fullscreen)); surface.with_pending_state(|state| state.states.unset(ToplevelState::Fullscreen));
@@ -391,14 +385,9 @@ pub fn apply_initial_window_rules(state: &mut Ferese, window: &Window) {
}) })
}); });
if initial { if initial {
let parent = toplevel.parent().and_then(|parent| { let parent = toplevel
state.windows.ids().iter().find_map(|(candidate, id)| { .parent()
candidate .and_then(|parent| state.windows.id_for_surface(&parent));
.toplevel()
.is_some_and(|surface| surface.wl_surface() == &parent)
.then_some(*id)
})
});
state.add_rule_placed_window(window.clone(), app_id.as_deref(), title.as_deref(), parent); state.add_rule_placed_window(window.clone(), app_id.as_deref(), title.as_deref(), parent);
} }
state.apply_initial_window_rules(window, app_id.as_deref(), title.as_deref(), transient); state.apply_initial_window_rules(window, app_id.as_deref(), title.as_deref(), transient);
+33 -18
View File
@@ -364,18 +364,14 @@ impl Ferese {
.into_iter() .into_iter()
.map(|symbol| symbol.raw()) .map(|symbol| symbol.raw())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let action = data.bindings.iter().find_map(|binding| { let action = data.bindings.action_for_key(
binding
.matches(
keycode, keycode,
&raw_symbols, &raw_symbols,
modifiers.logo, modifiers.logo,
modifiers.ctrl, modifiers.ctrl,
modifiers.alt, modifiers.alt,
modifiers.shift, modifiers.shift,
) );
.then(|| binding.action.clone())
});
if data.focus_cycle.is_some() if data.focus_cycle.is_some()
&& state == KeyState::Pressed && state == KeyState::Pressed
&& !matches!(action, Some(BindingAction::FocusMru(_))) && !matches!(action, Some(BindingAction::FocusMru(_)))
@@ -405,6 +401,8 @@ impl Ferese {
if state == KeyState::Pressed { if state == KeyState::Pressed {
data.intercepted_keys.insert(keycode); data.intercepted_keys.insert(keycode);
let action = action.clone();
if let BindingAction::FocusMru(reverse) = action { if let BindingAction::FocusMru(reverse) = action {
data.cycle_focus(reverse, modifiers.alt); data.cycle_focus(reverse, modifiers.alt);
} else { } else {
@@ -814,6 +812,8 @@ impl Ferese {
return; return;
} }
let previous = self.focused_window;
if let Some(window) = self.window_under_visual(position) { if let Some(window) = self.window_under_visual(position) {
let focused = self.windows.ids().get(&window).copied(); let focused = self.windows.ids().get(&window).copied();
if let Some(focused) = focused { if let Some(focused) = focused {
@@ -833,8 +833,21 @@ impl Ferese {
} else { } else {
// Hover transfers keyboard focus without changing the persistent // Hover transfers keyboard focus without changing the persistent
// stack: an exposed window must not cover the floats above it. // stack: an exposed window must not cover the floats above it.
for mapped in self.space.elements() { if let Some(previous) = previous
mapped.set_activated(mapped == &window); .filter(|id| Some(*id) != focused)
.and_then(|id| self.windows.window(id))
{
previous.set_activated(false);
if let Some(toplevel) = previous.toplevel() {
toplevel.send_pending_configure();
}
}
window.set_activated(true);
if let Some(toplevel) = window.toplevel() {
toplevel.send_pending_configure();
} }
} }
let surface = window let surface = window
@@ -844,16 +857,18 @@ impl Ferese {
.clone(); .clone();
keyboard.set_focus(self, Some(surface), serial); keyboard.set_focus(self, Some(surface), serial);
} else { } else {
if let Some(previous) = previous.and_then(|id| self.windows.window(id)) {
previous.set_activated(false);
if let Some(toplevel) = previous.toplevel() {
toplevel.send_pending_configure();
}
}
self.focused_window = None; self.focused_window = None;
keyboard.set_focus(self, Option::<WlSurface>::None, serial); keyboard.set_focus(self, Option::<WlSurface>::None, serial);
} }
self.space.elements().for_each(|window| {
if let Some(toplevel) = window.toplevel() {
toplevel.send_pending_configure();
}
});
if raise { if raise {
self.relayout(); self.relayout();
} else { } else {
@@ -987,11 +1002,11 @@ impl Ferese {
fn execute_binding(&mut self, action: BindingAction) { fn execute_binding(&mut self, action: BindingAction) {
match action { match action {
BindingAction::None => {} BindingAction::None => {}
BindingAction::Spawn(mut argv) => { BindingAction::Spawn(argv) => {
let program = argv.remove(0); let (program, args) = argv.split_first().expect("binding commands are validated");
match Command::new(&program) match Command::new(program)
.args(argv) .args(args)
.env("WAYLAND_DISPLAY", &self.socket_name) .env("WAYLAND_DISPLAY", &self.socket_name)
.env_remove("WAYLAND_SOCKET") .env_remove("WAYLAND_SOCKET")
.spawn() .spawn()
+93 -34
View File
@@ -84,8 +84,17 @@ pub(crate) fn init_font_loader(
Ok(()) Ok(())
} }
type OverviewStateLabel = type LabelBuffer = (MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>);
HashMap<(String, u64, [u32; 4]), (MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)>; type LabelStyle = (u64, [u32; 4]);
type OverviewStateLabel = HashMap<String, HashMap<LabelStyle, LabelBuffer>>;
#[derive(Debug)]
struct WindowLabel {
revision: u64,
limit: usize,
style: LabelStyle,
buffer: LabelBuffer,
}
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct OverviewState { pub(crate) struct OverviewState {
@@ -97,6 +106,8 @@ pub(crate) struct OverviewState {
font_requests: Option<FontRequests>, font_requests: Option<FontRequests>,
font: Option<FontArc>, font: Option<FontArc>,
labels: OverviewStateLabel, labels: OverviewStateLabel,
window_labels: HashMap<WindowId, WindowLabel>,
label_count: usize,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -118,6 +129,8 @@ impl Default for OverviewState {
font_requests: None, font_requests: None,
font: None, font: None,
labels: HashMap::new(), labels: HashMap::new(),
window_labels: HashMap::new(),
label_count: 0,
} }
} }
} }
@@ -163,6 +176,17 @@ fn centered_row_start(strip: Rect, count: usize, card_width: f64) -> f64 {
} }
impl OverviewState { impl OverviewState {
pub(crate) fn needs_tick(&self) -> bool {
self.motion.exit_transition.is_some()
|| self.motion.opacity.current != self.motion.opacity.target
|| self.motion.opacity.velocity != 0.0
|| self
.motion
.presentations
.values()
.any(|rect| rect.current != rect.target || rect.velocity != ferese_animation::RectVelocity::default())
}
pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool { pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool {
self.motion.is_animating(spring) self.motion.is_animating(spring)
} }
@@ -200,6 +224,8 @@ impl OverviewState {
} }
self.font = font; self.font = font;
self.labels.clear(); self.labels.clear();
self.window_labels.clear();
self.label_count = 0;
true true
} }
@@ -209,8 +235,8 @@ impl OverviewState {
scale: f64, scale: f64,
color: [f32; 4], color: [f32; 4],
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> { ) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
let key = (text.to_owned(), scale.to_bits(), color.map(f32::to_bits)); let key = (scale.to_bits(), color.map(f32::to_bits));
if let Some(buffer) = self.labels.get(&key) { if let Some(buffer) = self.labels.get(text).and_then(|styles| styles.get(&key)) {
return Some(buffer.clone()); return Some(buffer.clone());
} }
let font = self.font.as_ref()?; let font = self.font.as_ref()?;
@@ -249,11 +275,16 @@ impl OverviewState {
} }
let buffer = let buffer =
MemoryRenderBuffer::from_slice(&pixels, Fourcc::Abgr8888, (width, height), 1, Transform::Normal, None); MemoryRenderBuffer::from_slice(&pixels, Fourcc::Abgr8888, (width, height), 1, Transform::Normal, None);
if self.labels.len() >= 128 { if self.label_count >= 128 {
self.labels.clear(); self.labels.clear();
self.label_count = 0;
} }
let label = (buffer, (width, height).into()); let label = (buffer, (width, height).into());
self.labels.insert(key, label.clone()); self.labels
.entry(text.to_owned())
.or_default()
.insert(key, label.clone());
self.label_count += 1;
Some(label) Some(label)
} }
@@ -430,32 +461,53 @@ impl Ferese {
} }
} }
pub(crate) fn overview_window_label( pub(crate) fn retain_overview_labels(&mut self) {
&mut self,
window: &smithay::desktop::Window,
scale: f64,
width: f64,
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
let surface = window.toplevel()?.wl_surface();
let title = with_states(surface, |states| {
let data = states.data_map.get::<XdgToplevelSurfaceData>()?.lock().ok()?;
data.title
.clone()
.filter(|title| !title.trim().is_empty())
.or_else(|| data.app_id.clone())
})
.unwrap_or_else(|| "Window".into());
let limit = ((width - 16.0) / 8.0).clamp(1.0, 48.0) as usize;
let mut chars = title.chars().filter(|c| !c.is_control());
let mut title: String = chars.by_ref().take(limit).collect();
if chars.next().is_some() {
title.pop();
title.push('…');
}
self.overview self.overview
.label(&title, scale, self.theme_settings.text_primary_color.0) .window_labels
.retain(|id, _| self.windows.window(*id).is_some());
}
pub(crate) fn overview_window_label(&mut self, id: WindowId, scale: f64, width: f64) -> Option<LabelBuffer> {
let record = self.windows.record(id)?;
let limit = ((width - 16.0) / 8.0).clamp(1.0, 48.0) as usize;
let color = self.theme_settings.text_primary_color.0;
let style = (scale.to_bits(), color.map(f32::to_bits));
if let Some(label) = self.overview.window_labels.get(&id)
&& label.revision == record.metadata_revision
&& label.limit == limit
&& label.style == style
{
return Some(label.buffer.clone());
}
let title = if !record.title.trim().is_empty() {
&record.title
} else if !record.app_id.is_empty() {
&record.app_id
} else {
"Window"
};
let mut chars = title.chars().filter(|c| !c.is_control());
let mut text: String = chars.by_ref().take(limit).collect();
if chars.next().is_some() {
text.pop();
text.push('…');
}
let revision = record.metadata_revision;
let buffer = self.overview.label(&text, scale, color)?;
self.overview.window_labels.insert(
id,
WindowLabel {
revision,
limit,
style,
buffer: buffer.clone(),
},
);
Some(buffer)
} }
pub(crate) fn overview_workspace_label( pub(crate) fn overview_workspace_label(
@@ -608,9 +660,7 @@ impl Ferese {
let capacity = strip_capacity(strip); let capacity = strip_capacity(strip);
let workspaces = self let workspaces = self
.output_workspaces .output_workspaces
.workspace_views(&self.workspaces) .workspace_views_for_output(&self.workspaces, output_id)
.into_iter()
.filter(|view| view.output == output_id)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let selected_index = workspaces let selected_index = workspaces
.iter() .iter()
@@ -904,6 +954,15 @@ fn directional_distance(direction: Direction, current: (f64, f64), candidate: (f
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn tiny_overview_target_changes_still_get_a_settling_tick() {
let mut overview = OverviewState::default();
assert!(!overview.needs_tick());
overview.motion.opacity.set_target(0.0001);
assert!(!overview.is_animating(SpringConfig::default()));
assert!(overview.needs_tick());
}
fn test_font() -> FontArc { fn test_font() -> FontArc {
FontArc::try_from_slice(include_bytes!("../../assets/fonts/Comfortaa-Regular.otf")).unwrap() FontArc::try_from_slice(include_bytes!("../../assets/fonts/Comfortaa-Regular.otf")).unwrap()
} }
+3 -2
View File
@@ -5,7 +5,6 @@ use std::time::{Duration, Instant};
use smithay::backend::renderer::element::{ use smithay::backend::renderer::element::{
Element, Id, Kind, RenderElement, RenderElementPresentationState, RenderElementStates, Element, Id, Kind, RenderElement, RenderElementPresentationState, RenderElementStates,
}; };
use smithay::backend::renderer::gles::element::PixelShaderElement;
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform}; use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet}; use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
use smithay::desktop::utils::OutputPresentationFeedback; use smithay::desktop::utils::OutputPresentationFeedback;
@@ -15,6 +14,8 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform}; use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform};
use smithay::wayland::compositor::SurfaceData; use smithay::wayland::compositor::SurfaceData;
use crate::render::SharedPixelShaderElement;
pub(crate) const HANDOFF: Duration = Duration::from_millis(80); pub(crate) const HANDOFF: Duration = Duration::from_millis(80);
pub(crate) const SNAPSHOT_BUDGET: usize = 64 * 1024 * 1024; pub(crate) const SNAPSHOT_BUDGET: usize = 64 * 1024 * 1024;
@@ -164,7 +165,7 @@ pub(crate) fn advance_handoff(
/// A shader canvas whose damage/paint destination uses the shared pixel edges. /// A shader canvas whose damage/paint destination uses the shared pixel edges.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct PhysicalShaderElement { pub(crate) struct PhysicalShaderElement {
pub inner: PixelShaderElement, pub inner: SharedPixelShaderElement,
pub geometry: Rectangle<i32, Physical>, pub geometry: Rectangle<i32, Physical>,
} }
+24 -24
View File
@@ -130,7 +130,7 @@ pub(super) fn window_border_element(
let context = renderer.context_id().erased(); let context = renderer.context_id().erased();
let buffers = &mut resources.windows.entry(id).or_default().borders; let buffers = &mut resources.windows.entry(id).or_default().borders;
if !buffers.contexts.contains_key(&context) { if !buffers.contexts.contains_key(&context) {
let element = PixelShaderElement::new( let element = SharedPixelShaderElement::new(
programs.border.clone(), programs.border.clone(),
geometry, geometry,
None, None,
@@ -164,16 +164,16 @@ pub(super) fn window_border_element(
pub(super) fn border_uniforms(parameters: &BorderParameters) -> Vec<Uniform<'static>> { pub(super) fn border_uniforms(parameters: &BorderParameters) -> Vec<Uniform<'static>> {
vec![ vec![
Uniform::new("clip_rect", parameters.clip_rect).into_owned(), Uniform::new("clip_rect", parameters.clip_rect),
Uniform::new("radius", parameters.radius).into_owned(), Uniform::new("radius", parameters.radius),
Uniform::new("border_width", parameters.width).into_owned(), Uniform::new("border_width", parameters.width),
Uniform::new("border_color", parameters.color).into_owned(), Uniform::new("border_color", parameters.color),
Uniform::new("border_color_to", parameters.color_to).into_owned(), Uniform::new("border_color_to", parameters.color_to),
Uniform::new("gradient_line", parameters.gradient_line).into_owned(), Uniform::new("gradient_line", parameters.gradient_line),
Uniform::new("focus_color", parameters.focus_color).into_owned(), Uniform::new("focus_color", parameters.focus_color),
Uniform::new("focus_color_to", parameters.focus_color_to).into_owned(), Uniform::new("focus_color_to", parameters.focus_color_to),
Uniform::new("focus_gradient_line", parameters.focus_gradient_line).into_owned(), Uniform::new("focus_gradient_line", parameters.focus_gradient_line),
Uniform::new("focus_mix", parameters.focus_mix).into_owned(), Uniform::new("focus_mix", parameters.focus_mix),
] ]
} }
@@ -213,12 +213,12 @@ pub(super) fn window_tint_element(
}; };
let uniforms = |p: &BorderParameters| { let uniforms = |p: &BorderParameters| {
vec![ vec![
Uniform::new("visible_rect", p.clip_rect).into_owned(), Uniform::new("visible_rect", p.clip_rect),
Uniform::new("material_radius", p.radius).into_owned(), Uniform::new("material_radius", p.radius),
Uniform::new("tint", p.color).into_owned(), Uniform::new("tint", p.color),
Uniform::new("paint_mode", 0.0_f32).into_owned(), Uniform::new("paint_mode", 0.0_f32),
Uniform::new("shadow_rect", p.clip_rect).into_owned(), Uniform::new("shadow_rect", p.clip_rect),
Uniform::new("shadow_values", [0.0_f32; 2]).into_owned(), Uniform::new("shadow_values", [0.0_f32; 2]),
] ]
}; };
let context = renderer.context_id().erased(); let context = renderer.context_id().erased();
@@ -228,7 +228,7 @@ pub(super) fn window_tint_element(
&mut resources.windows.entry(id).or_default().dim &mut resources.windows.entry(id).or_default().dim
}; };
let cached = buffers.contexts.entry(context).or_insert_with(|| CachedBorder { let cached = buffers.contexts.entry(context).or_insert_with(|| CachedBorder {
element: PixelShaderElement::new( element: SharedPixelShaderElement::new(
program.0, program.0,
geometry, geometry,
None, None,
@@ -293,7 +293,7 @@ pub(super) fn window_shadow_element(
let context = renderer.context_id().erased(); let context = renderer.context_id().erased();
let buffers = &mut resources.windows.entry(id).or_default().shadow; let buffers = &mut resources.windows.entry(id).or_default().shadow;
if !buffers.contexts.contains_key(&context) { if !buffers.contexts.contains_key(&context) {
let element = PixelShaderElement::new( let element = SharedPixelShaderElement::new(
programs.shadow.clone(), programs.shadow.clone(),
bounds, bounds,
None, None,
@@ -331,11 +331,11 @@ pub(super) fn window_shadow_element(
pub(super) fn shadow_uniforms(parameters: &ShadowParameters) -> Vec<Uniform<'static>> { pub(super) fn shadow_uniforms(parameters: &ShadowParameters) -> Vec<Uniform<'static>> {
vec![ vec![
Uniform::new("shadow_rect", parameters.shadow_rect).into_owned(), Uniform::new("shadow_rect", parameters.shadow_rect),
Uniform::new("radius", parameters.radius).into_owned(), Uniform::new("radius", parameters.radius),
Uniform::new("blur", parameters.blur).into_owned(), Uniform::new("blur", parameters.blur),
Uniform::new("opacity", parameters.opacity).into_owned(), Uniform::new("opacity", parameters.opacity),
Uniform::new("shadow_color", parameters.color).into_owned(), Uniform::new("shadow_color", parameters.color),
] ]
} }
+16 -16
View File
@@ -248,7 +248,7 @@ pub(super) fn material_element(
capture.dirty.clone(), capture.dirty.clone(),
)) ))
} else { } else {
MaterialElement::Fill(PixelShaderElement::new( MaterialElement::Fill(SharedPixelShaderElement::new(
program.0.clone(), program.0.clone(),
geometry, geometry,
parameters.opaque.then(|| vec![Rectangle::from_size(geometry.size)]), parameters.opaque.then(|| vec![Rectangle::from_size(geometry.size)]),
@@ -264,7 +264,7 @@ pub(super) fn material_element(
.or_insert_with(|| CachedMaterial { .or_insert_with(|| CachedMaterial {
element: make_element(), element: make_element(),
parameters: parameters.clone(), parameters: parameters.clone(),
shadow: PixelShaderElement::new( shadow: SharedPixelShaderElement::new(
program.0.clone(), program.0.clone(),
parameters.shadow_bounds, parameters.shadow_bounds,
None, None,
@@ -356,14 +356,14 @@ pub(super) fn blur_uniform_names() -> [UniformName<'static>; 8] {
pub(super) fn blur_uniforms(p: &MaterialParameters) -> Vec<Uniform<'static>> { pub(super) fn blur_uniforms(p: &MaterialParameters) -> Vec<Uniform<'static>> {
vec![ vec![
Uniform::new("visible_rect", p.visible_framebuffer).into_owned(), Uniform::new("visible_rect", p.visible_framebuffer),
Uniform::new("material_radius", p.radius).into_owned(), Uniform::new("material_radius", p.radius),
Uniform::new("texture_size", [p.sample_framebuffer[2], p.sample_framebuffer[3]]).into_owned(), Uniform::new("texture_size", [p.sample_framebuffer[2], p.sample_framebuffer[3]]),
Uniform::new("capture_origin", [p.sample_framebuffer[0], p.sample_framebuffer[1]]).into_owned(), Uniform::new("capture_origin", [p.sample_framebuffer[0], p.sample_framebuffer[1]]),
Uniform::new("blur_radius", p.blur).into_owned(), Uniform::new("blur_radius", p.blur),
Uniform::new("presentation_alpha", p.presentation_alpha).into_owned(), Uniform::new("presentation_alpha", p.presentation_alpha),
Uniform::new("background_opacity", p.background_opacity).into_owned(), Uniform::new("background_opacity", p.background_opacity),
Uniform::new("tint", p.tint).into_owned(), Uniform::new("tint", p.tint),
] ]
} }
@@ -426,12 +426,12 @@ pub(super) fn material_uniforms(parameters: &MaterialParameters) -> Vec<Uniform<
pub(super) fn decoration_uniforms(parameters: &MaterialParameters, paint_mode: f32) -> Vec<Uniform<'static>> { pub(super) fn decoration_uniforms(parameters: &MaterialParameters, paint_mode: f32) -> Vec<Uniform<'static>> {
vec![ vec![
Uniform::new("paint_mode", paint_mode).into_owned(), Uniform::new("paint_mode", paint_mode),
Uniform::new("shadow_rect", parameters.shadow_rect).into_owned(), Uniform::new("shadow_rect", parameters.shadow_rect),
Uniform::new("shadow_values", parameters.shadow_values).into_owned(), Uniform::new("shadow_values", parameters.shadow_values),
Uniform::new("visible_rect", parameters.visible_framebuffer).into_owned(), Uniform::new("visible_rect", parameters.visible_framebuffer),
Uniform::new("material_radius", parameters.radius).into_owned(), Uniform::new("material_radius", parameters.radius),
Uniform::new("tint", parameters.tint).into_owned(), Uniform::new("tint", parameters.tint),
] ]
} }
+10 -9
View File
@@ -4,6 +4,7 @@ mod materials;
mod overview; mod overview;
mod resources; mod resources;
mod scene; mod scene;
mod shader;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@@ -18,6 +19,7 @@ use scene::cursor_elements;
pub(crate) use scene::{ pub(crate) use scene::{
animated_window_elements, frame_effect_metrics, layer_surfaces, output_elements, sampled_output_elements, animated_window_elements, frame_effect_metrics, layer_surfaces, output_elements, sampled_output_elements,
}; };
pub(crate) use shader::SharedPixelShaderElement;
use smithay::backend::allocator::Fourcc; use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement; use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
use smithay::backend::renderer::element::solid::SolidColorRenderElement; use smithay::backend::renderer::element::solid::SolidColorRenderElement;
@@ -31,7 +33,6 @@ use smithay::backend::renderer::element::utils::{
use smithay::backend::renderer::element::{ use smithay::backend::renderer::element::{
Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage, render_elements, Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage, render_elements,
}; };
use smithay::backend::renderer::gles::element::PixelShaderElement;
use smithay::backend::renderer::gles::{ use smithay::backend::renderer::gles::{
GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, UniformName, GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, UniformName,
UniformType, UniformType,
@@ -66,7 +67,7 @@ render_elements! {
Memory=MemoryRenderElement, Memory=MemoryRenderElement,
Solid=SolidColorRenderElement, Solid=SolidColorRenderElement,
LockSurface=WaylandSurfaceRenderElement<GlesRenderer>, LockSurface=WaylandSurfaceRenderElement<GlesRenderer>,
Border=PixelShaderElement, Border=SharedPixelShaderElement,
Blur=BlurRenderElement, Blur=BlurRenderElement,
Effect=PhysicalShaderElement, Effect=PhysicalShaderElement,
Native=NativeTextureElement, Native=NativeTextureElement,
@@ -132,7 +133,7 @@ struct BlurRenderElement {
commit: CommitCounter, commit: CommitCounter,
geometry: Rectangle<i32, Logical>, geometry: Rectangle<i32, Logical>,
capture_rect: [i32; 4], capture_rect: [i32; 4],
uniforms: Vec<Uniform<'static>>, uniforms: Arc<[Uniform<'static>]>,
} }
impl BlurRenderElement { impl BlurRenderElement {
@@ -150,7 +151,7 @@ impl BlurRenderElement {
commit: CommitCounter::default(), commit: CommitCounter::default(),
geometry: parameters.sample_geometry, geometry: parameters.sample_geometry,
capture_rect: framebuffer_capture_rect(parameters.sample_framebuffer), capture_rect: framebuffer_capture_rect(parameters.sample_framebuffer),
uniforms: blur_uniforms(parameters), uniforms: blur_uniforms(parameters).into(),
} }
} }
@@ -158,7 +159,7 @@ impl BlurRenderElement {
self.capture_dirty.store(true, Ordering::Relaxed); self.capture_dirty.store(true, Ordering::Relaxed);
self.geometry = parameters.sample_geometry; self.geometry = parameters.sample_geometry;
self.capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer); self.capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer);
self.uniforms = blur_uniforms(parameters); self.uniforms = blur_uniforms(parameters).into();
self.commit.increment(); self.commit.increment();
} }
} }
@@ -327,7 +328,7 @@ struct MaterialParameters {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum MaterialElement { enum MaterialElement {
Fill(PixelShaderElement), Fill(SharedPixelShaderElement),
Blur(BlurRenderElement), Blur(BlurRenderElement),
} }
@@ -335,7 +336,7 @@ enum MaterialElement {
struct CachedMaterial { struct CachedMaterial {
element: MaterialElement, element: MaterialElement,
parameters: MaterialParameters, parameters: MaterialParameters,
shadow: PixelShaderElement, shadow: SharedPixelShaderElement,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
@@ -361,7 +362,7 @@ struct BorderParameters {
#[derive(Debug)] #[derive(Debug)]
struct CachedBorder { struct CachedBorder {
element: PixelShaderElement, element: SharedPixelShaderElement,
parameters: BorderParameters, parameters: BorderParameters,
} }
@@ -382,7 +383,7 @@ struct ShadowParameters {
#[derive(Debug)] #[derive(Debug)]
struct CachedShadow { struct CachedShadow {
element: PixelShaderElement, element: SharedPixelShaderElement,
parameters: ShadowParameters, parameters: ShadowParameters,
} }
+17 -19
View File
@@ -40,12 +40,12 @@ pub(super) fn overview_chrome_element(
border_uniforms(p) border_uniforms(p)
} else { } else {
vec![ vec![
Uniform::new("visible_rect", p.clip_rect).into_owned(), Uniform::new("visible_rect", p.clip_rect),
Uniform::new("material_radius", p.radius).into_owned(), Uniform::new("material_radius", p.radius),
Uniform::new("tint", p.color).into_owned(), Uniform::new("tint", p.color),
Uniform::new("paint_mode", 0.0_f32).into_owned(), Uniform::new("paint_mode", 0.0_f32),
Uniform::new("shadow_rect", p.clip_rect).into_owned(), Uniform::new("shadow_rect", p.clip_rect),
Uniform::new("shadow_values", [0.0_f32; 2]).into_owned(), Uniform::new("shadow_values", [0.0_f32; 2]),
] ]
} }
}; };
@@ -61,7 +61,7 @@ pub(super) fn overview_chrome_element(
.or_default() .or_default()
.entry(context) .entry(context)
.or_insert_with(|| CachedBorder { .or_insert_with(|| CachedBorder {
element: PixelShaderElement::new( element: SharedPixelShaderElement::new(
program, program,
geometry, geometry,
None, None,
@@ -119,33 +119,31 @@ pub(super) fn overview_strip_elements(
.opacity; .opacity;
let mut elements = Vec::new(); let mut elements = Vec::new();
let programs = rounded_clip_program(&mut state.render, renderer); let programs = rounded_clip_program(&mut state.render, renderer);
// Resolve IDs once per strip, rather than scanning all managed windows state.retain_overview_labels();
// separately for every miniature. Own the handles so rendering can mutate state.
let windows_by_id: std::collections::HashMap<_, _> = state let card_ids = cards
.windows
.ids()
.iter() .iter()
.map(|(window, id)| (*id, window.clone())) .map(|card| card.workspace.0)
.collect(); .collect::<std::collections::HashSet<_>>();
if let Some(output_id) = state.output_id(output) if let Some(output_id) = state.output_id(output)
&& let Some(cache) = state.render.outputs.get_mut(&output_id) && let Some(cache) = state.render.outputs.get_mut(&output_id)
{ {
cache.chrome.retain(|(id, part), _| match part { cache.chrome.retain(|(id, part), _| match part {
OverviewChromePart::Caption => windows_by_id.keys().any(|window| window.0 == *id), OverviewChromePart::Caption => state.windows.window(ferese_layout::WindowId(*id)).is_some(),
OverviewChromePart::Card | OverviewChromePart::Outline => cards.iter().any(|card| card.workspace.0 == *id), OverviewChromePart::Card | OverviewChromePart::Outline => card_ids.contains(id),
OverviewChromePart::Strip => true, OverviewChromePart::Strip => true,
}); });
} }
// Cached title textures are independent of window content and stay readable // Cached title textures are independent of window content and stay readable
// when thumbnails are small. Their pills use the shell's material and radius. // when thumbnails are small. Their pills use the shell's material and radius.
for (id, window) in &windows_by_id { for id in frame.windows.keys() {
if !state.window_belongs_to_output(*id, output) { if !state.window_belongs_to_output(*id, output) {
continue; continue;
} }
let Some(rect) = frame.windows.get(id).map(|sample| sample.rect) else { let Some(rect) = frame.windows.get(id).map(|sample| sample.rect) else {
continue; continue;
}; };
let Some((buffer, size)) = state.overview_window_label(window, scale, rect.width) else { let Some((buffer, size)) = state.overview_window_label(*id, scale, rect.width) else {
continue; continue;
}; };
let width = (f64::from(size.w) / scale + 16.0).min(rect.width); let width = (f64::from(size.w) / scale + 16.0).min(rect.width);
@@ -222,7 +220,7 @@ pub(super) fn overview_strip_elements(
} }
if let Some(programs) = &programs { if let Some(programs) = &programs {
for (id, rect) in &card.windows { for (id, rect) in &card.windows {
if let Some(window) = windows_by_id.get(id) { if let Some(window) = state.windows.window(*id) {
elements.extend(rounded_window_elements( elements.extend(rounded_window_elements(
renderer, renderer,
window, window,
+25 -23
View File
@@ -1,3 +1,5 @@
use smithay::desktop::Window;
use super::*; use super::*;
pub(crate) fn animated_window_elements( pub(crate) fn animated_window_elements(
@@ -109,17 +111,7 @@ pub(crate) fn sampled_output_elements(
frame, frame,
)); ));
} }
let mut candidates = if state.overview.is_active() { let prepare_window = |window: &Window| {
state.windows.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.windows.ids().get(window).map_or(0, |id| id.0)));
}
let windows = candidates
.iter()
.filter_map(|window| {
// Configure immediately, but present the frame/shadow only after // Configure immediately, but present the frame/shadow only after
// the first buffer commit has settled the actual client geometry. // the first buffer commit has settled the actual client geometry.
if !state.window_content_ready(window) { if !state.window_content_ready(window) {
@@ -137,11 +129,24 @@ pub(crate) fn sampled_output_elements(
let close_alpha = sample.close_alpha; let close_alpha = sample.close_alpha;
let decoration_progress = sample.geometry.decorations.clamp(0.0, 1.0); let decoration_progress = sample.geometry.decorations.clamp(0.0, 1.0);
Some((window.clone(), id, visual, decoration_progress, close_alpha)) Some((window.clone(), id, sample, visual, decoration_progress, close_alpha))
}) };
.collect::<Vec<_>>(); let windows = if state.overview.is_active() {
state
.windows
.overview_windows()
.filter_map(prepare_window)
.collect::<Vec<_>>()
} else {
state
.space
.elements()
.rev()
.filter_map(prepare_window)
.collect::<Vec<_>>()
};
for (window, id, visual, decoration_progress, close_alpha) in windows { for (window, id, sample, visual, decoration_progress, close_alpha) in windows {
let constrain = rounded_visual_rect(visual, output_geometry.loc); let constrain = rounded_visual_rect(visual, output_geometry.loc);
let material_surface = window let material_surface = window
.toplevel() .toplevel()
@@ -158,10 +163,10 @@ pub(crate) fn sampled_output_elements(
let corners = RoundedRect::new(visual, output_geometry.loc, scale, window_radius); let corners = RoundedRect::new(visual, output_geometry.loc, scale, window_radius);
let pixels = corners.rect; let pixels = corners.rect;
// Only overview/close intentionally scale the complete application. // Only overview/close intentionally scale the complete application.
let scale_content = frame.overview.is_presenting() || frame.windows[&id].close_scale != 1.0; let scale_content = frame.overview.is_presenting() || sample.close_scale != 1.0;
let behavior = resize_content_behavior(scale_content); let behavior = resize_content_behavior(scale_content);
let dim = frame.windows[&id].dim; let dim = sample.dim;
if let Some(overlay) = window_tint_element( if let Some(overlay) = window_tint_element(
&mut state.render, &mut state.render,
renderer, renderer,
@@ -182,7 +187,7 @@ pub(crate) fn sampled_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 focus = frame.windows[&id].focus; let focus = sample.focus;
let border_width = state.theme_settings.border_width let border_width = state.theme_settings.border_width
+ (state.theme_settings.focus_ring_width - state.theme_settings.border_width) * focus; + (state.theme_settings.focus_ring_width - state.theme_settings.border_width) * focus;
let border_color = state.theme_settings.border_color.0; let border_color = state.theme_settings.border_color.0;
@@ -245,10 +250,7 @@ pub(crate) fn sampled_output_elements(
))), ))),
alpha: crate::presentation::handoff_alpha(snapshot.elapsed) * close_alpha, alpha: crate::presentation::handoff_alpha(snapshot.elapsed) * close_alpha,
program: Some(programs.texture.clone()), program: Some(programs.texture.clone()),
uniforms: vec![ uniforms: vec![Uniform::new("clip_rect", clip), Uniform::new("radius", corners.radius)],
Uniform::new("clip_rect", clip).into_owned(),
Uniform::new("radius", corners.radius).into_owned(),
],
} }
.into(), .into(),
); );
@@ -260,7 +262,7 @@ pub(crate) fn sampled_output_elements(
corners, corners,
scale, scale,
close_alpha, close_alpha,
frame.windows[&id].geometry.presentation_changed, sample.geometry.presentation_changed,
output, output,
programs.clone(), programs.clone(),
behavior, behavior,
+231
View File
@@ -0,0 +1,231 @@
//! Static uniform names stay borrowed; cached frame elements share their state.
//! Copy on write preserves the ID and commit seen by an already sampled frame.
use std::sync::Arc;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, Uniform};
use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions};
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
#[derive(Clone, Debug)]
pub(crate) struct SharedPixelShaderElement {
shader: GlesPixelProgram,
state: Arc<ShaderState>,
}
#[derive(Clone, Debug)]
struct ShaderState {
id: Id,
commit: CommitCounter,
area: Rectangle<i32, Logical>,
opaque: Vec<Rectangle<i32, Logical>>,
alpha: f32,
uniforms: Vec<Uniform<'static>>,
kind: Kind,
}
impl SharedPixelShaderElement {
pub(crate) fn new(
shader: GlesPixelProgram,
area: Rectangle<i32, Logical>,
opaque: Option<Vec<Rectangle<i32, Logical>>>,
alpha: f32,
uniforms: Vec<Uniform<'static>>,
kind: Kind,
) -> Self {
Self {
shader,
state: Arc::new(ShaderState {
id: Id::new(),
commit: CommitCounter::default(),
area,
opaque: opaque.unwrap_or_default(),
alpha,
uniforms,
kind,
}),
}
}
pub(crate) fn resize(&mut self, area: Rectangle<i32, Logical>, opaque: Option<Vec<Rectangle<i32, Logical>>>) {
let opaque = opaque.unwrap_or_default();
if self.state.area != area || self.state.opaque != opaque {
let state = Arc::make_mut(&mut self.state);
state.area = area;
state.opaque = opaque;
state.commit.increment();
}
}
pub(crate) fn update_uniforms(&mut self, uniforms: Vec<Uniform<'static>>) {
let state = Arc::make_mut(&mut self.state);
state.uniforms = uniforms;
state.commit.increment();
}
}
impl Element for SharedPixelShaderElement {
fn id(&self) -> &Id {
&self.state.id
}
fn current_commit(&self) -> CommitCounter {
self.state.commit
}
fn src(&self) -> Rectangle<f64, Buffer> {
Rectangle::from_size(self.state.area.size.to_f64().to_buffer(1.0, Transform::Normal))
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.state.area.to_physical_precise_round(scale)
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
self.state
.opaque
.iter()
.map(|region| region.to_physical_precise_round(scale))
.collect()
}
fn alpha(&self) -> f32 {
self.state.alpha
}
fn kind(&self) -> Kind {
self.state.kind
}
}
impl RenderElement<GlesRenderer> for SharedPixelShaderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
_opaque: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
frame.render_pixel_shader_to(
&self.shader,
src,
dst,
self.state.area.size.to_buffer(1, Transform::Normal),
Some(damage),
self.state.alpha,
&self.state.uniforms,
)
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use smithay::backend::allocator::Fourcc;
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
use smithay::backend::renderer::gles::element::PixelShaderElement;
use smithay::backend::renderer::gles::{GlesTexture, UniformName, UniformType};
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer};
use super::*;
fn pixels(renderer: &mut GlesRenderer, element: &impl RenderElement<GlesRenderer>) -> Vec<u8> {
let size = (32, 32).into();
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap();
let rect = Rectangle::from_size((32, 32).into());
let mut target = renderer.bind(&mut texture).unwrap();
{
let mut frame = renderer
.render(&mut target, (32, 32).into(), Transform::Normal)
.unwrap();
frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap();
element.draw(&mut frame, element.src(), rect, &[rect], &[]).unwrap();
let _ = frame.finish().unwrap();
}
let mapping = renderer
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
.unwrap();
renderer.map_texture(&mapping).unwrap().to_vec()
}
#[test]
#[ignore = "requires an EGL rendering device"]
fn shared_shader_matches_smithay_and_keeps_old_frames_immutable() {
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
let display = unsafe { EGLDisplay::new(device).unwrap() };
let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
let program = renderer
.compile_custom_pixel_shader(
super::super::corner_shader(super::super::ROUNDED_SOLID_SHADER),
&[
UniformName::new("clip_rect", UniformType::_4f),
UniformName::new("radius", UniformType::_1f),
UniformName::new("color", UniformType::_4f),
],
)
.unwrap();
let area = Rectangle::new((2, 3).into(), (32, 32).into());
let uniforms = |color| {
vec![
Uniform::new("clip_rect", [0.0_f32, 0.0, 32.0, 32.0]),
Uniform::new("radius", 8.0_f32),
Uniform::new("color", color),
]
};
let red = [0.5_f32, 0.0, 0.0, 0.5];
let green = [0.0_f32, 0.5, 0.0, 0.5];
let opaque = Some(vec![Rectangle::from_size((20, 20).into())]);
let baseline = PixelShaderElement::new(
program.clone(),
area,
opaque.clone(),
0.5,
uniforms(red),
Kind::Unspecified,
);
let mut shared = SharedPixelShaderElement::new(program, area, opaque, 0.5, uniforms(red), Kind::Unspecified);
let old = shared.clone();
let old_commit = old.current_commit();
assert!(Arc::ptr_eq(&shared.state, &old.state));
assert!(
shared
.state
.uniforms
.iter()
.all(|uniform| matches!(uniform.name, Cow::Borrowed(_)))
);
for scale in [1.0, 1.25, 2.0] {
assert_eq!(shared.geometry(scale.into()), baseline.geometry(scale.into()));
assert_eq!(
&*shared.opaque_regions(scale.into()),
&*baseline.opaque_regions(scale.into())
);
}
assert_eq!(pixels(&mut renderer, &shared), pixels(&mut renderer, &baseline));
shared.resize(area, Some(vec![Rectangle::from_size((20, 20).into())]));
assert!(Arc::ptr_eq(&shared.state, &old.state));
shared.update_uniforms(uniforms(green));
assert!(!Arc::ptr_eq(&shared.state, &old.state));
assert_eq!(shared.id(), old.id());
assert_eq!(old.current_commit(), old_commit);
assert_ne!(shared.current_commit(), old_commit);
assert_eq!(pixels(&mut renderer, &old), pixels(&mut renderer, &baseline));
assert_ne!(pixels(&mut renderer, &shared), pixels(&mut renderer, &baseline));
let retained = shared.clone();
shared.resize(Rectangle::from_size((16, 16).into()), None);
assert_eq!(retained.geometry(1.0.into()), area.to_physical_precise_round(1.0));
assert_eq!(shared.geometry(1.0.into()).size, (16, 16).into());
assert!(shared.opaque_regions(1.0.into()).is_empty());
assert_eq!(shared.alpha(), baseline.alpha());
assert_eq!(shared.kind(), baseline.kind());
}
}
+152 -41
View File
@@ -4,8 +4,6 @@ use ferese_protocols::shell::v1::server::ferese_shell_manager_v1::FereseShellMan
use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1; use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1;
use ferese_protocols::shell::v1::server::{ferese_shell_manager_v1, ferese_shell_v1}; use ferese_protocols::shell::v1::server::{ferese_shell_manager_v1, ferese_shell_v1};
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource}; use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
use crate::Ferese; use crate::Ferese;
use crate::private_client::ClientCapabilities; use crate::private_client::ClientCapabilities;
@@ -270,10 +268,15 @@ impl Ferese {
self.last_shell_snapshot = None; self.last_shell_snapshot = None;
return; return;
} }
let snapshot = self.shell_snapshot(); if self
if self.last_shell_snapshot.as_ref() == Some(&snapshot) { .last_shell_snapshot
.as_ref()
.is_some_and(|snapshot| self.shell_snapshot_matches(snapshot))
{
return; return;
} }
let snapshot = self.shell_snapshot();
for resource in &self.shell_resources { for resource in &self.shell_resources {
if let Ok(shell) = resource.upgrade() { if let Ok(shell) = resource.upgrade() {
self.shell_snapshot_serial = self.shell_snapshot_serial.wrapping_add(1); self.shell_snapshot_serial = self.shell_snapshot_serial.wrapping_add(1);
@@ -283,6 +286,65 @@ impl Ferese {
self.last_shell_snapshot = Some(snapshot); self.last_shell_snapshot = Some(snapshot);
} }
fn shell_snapshot_matches(&self, snapshot: &ShellSnapshot) -> bool {
let mut outputs = self.output_workspaces.connected_outputs();
for previous in &snapshot.outputs {
if outputs.next() != Some(previous.id)
|| self.output_names.get(&previous.id) != Some(&previous.name)
|| self.output_workspaces.active_workspace(previous.id) != Some(previous.active_workspace)
|| (self.output_workspaces.focused_output() == Some(previous.id)) != previous.focused
{
return false;
}
}
if outputs.next().is_some() {
return false;
}
let mut workspaces = self.output_workspaces.workspace_views_iter(&self.workspaces);
for previous in &snapshot.workspaces {
let Some(view) = workspaces.next() else {
return false;
};
if previous.id != view.id
|| previous.output != Some(view.output)
|| previous.index != view.index
|| previous.window_count != view.window_count as u32
|| previous.visible != view.visible
|| previous.focused != view.focused
{
return false;
}
}
if workspaces.next().is_some() {
return false;
}
let mut windows = self.windows.ordered_ids().filter_map(|id| self.shell_window_data(id));
for previous in &snapshot.windows {
let Some((id, workspace, app_id, title, state)) = windows.next() else {
return false;
};
if previous.id != id
|| previous.workspace != workspace
|| previous.app_id != app_id
|| previous.title != title
|| previous.state != state
{
return false;
}
}
windows.next().is_none()
}
fn shell_snapshot(&self) -> ShellSnapshot { fn shell_snapshot(&self) -> ShellSnapshot {
ShellSnapshot { ShellSnapshot {
outputs: self.output_snapshots(), outputs: self.output_snapshots(),
@@ -319,8 +381,7 @@ impl Ferese {
fn workspace_snapshots(&self) -> Vec<WorkspaceSnapshot> { fn workspace_snapshots(&self) -> Vec<WorkspaceSnapshot> {
self.output_workspaces self.output_workspaces
.workspace_views(&self.workspaces) .workspace_views_iter(&self.workspaces)
.into_iter()
.map(|view| WorkspaceSnapshot { .map(|view| WorkspaceSnapshot {
id: view.id, id: view.id,
output: Some(view.output), output: Some(view.output),
@@ -333,56 +394,46 @@ impl Ferese {
.collect() .collect()
} }
fn managed_window_snapshots(&self) -> Vec<ManagedWindowSnapshot> { fn shell_window_data(&self, id: WindowId) -> Option<(WindowId, u64, &str, &str, ferese_shell_v1::WindowState)> {
let mut snapshots = self let workspace = self.workspaces.workspace_for_window(id)?;
.windows let record = self.windows.record(id)?;
.ids() self.windows.window(id)?.toplevel()?;
.iter()
.filter_map(|(window, id)| {
let workspace = self.workspaces.workspace_for_window(*id)?.0;
let toplevel = window.toplevel()?;
let (app_id, title) = with_states(toplevel.wl_surface(), |states| {
let attributes = states
.data_map
.get::<XdgToplevelSurfaceData>()
.expect("xdg toplevel state exists")
.lock()
.expect("xdg toplevel state is not poisoned");
(
attributes.app_id.clone().unwrap_or_default(),
attributes.title.clone().unwrap_or_default(),
)
});
let mut state = ferese_shell_v1::WindowState::empty(); let mut state = ferese_shell_v1::WindowState::empty();
if self.focused_window == Some(*id) { if self.focused_window == Some(id) {
state |= ferese_shell_v1::WindowState::Focused; state |= ferese_shell_v1::WindowState::Focused;
} }
if self if self
.workspaces .workspaces
.workspace_for_window(*id) .workspace(workspace)
.and_then(|workspace| self.workspaces.workspace(workspace)) .is_some_and(|workspace| workspace.fullscreen == Some(id))
.is_some_and(|workspace| workspace.fullscreen == Some(*id))
{ {
state |= ferese_shell_v1::WindowState::Fullscreen; state |= ferese_shell_v1::WindowState::Fullscreen;
} }
if self.is_floating_window(window) {
if matches!(
self.workspaces.placement(id),
Some(ferese_core::WindowPlacement::Floating { .. })
) {
state |= ferese_shell_v1::WindowState::Floating; state |= ferese_shell_v1::WindowState::Floating;
} }
Some(ManagedWindowSnapshot { Some((id, workspace.0, &record.app_id, &record.title, state))
id: *id, }
fn managed_window_snapshots(&self) -> Vec<ManagedWindowSnapshot> {
self.windows
.ordered_ids()
.filter_map(|id| self.shell_window_data(id))
.map(|(id, workspace, app_id, title, state)| ManagedWindowSnapshot {
id,
workspace, workspace,
app_id, app_id: app_id.to_owned(),
title, title: title.to_owned(),
state, state,
}) })
}) .collect()
.collect::<Vec<_>>();
snapshots.sort_by_key(|snapshot| snapshot.id.0);
snapshots
} }
fn handle_shell_window_request( fn handle_shell_window_request(
@@ -530,6 +581,66 @@ fn send_request_failed(shell: &FereseShellV1, request: ferese_shell_v1::FailedRe
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[test]
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
fn snapshot_precheck_detects_membership_selection_and_output_changes() {
use smithay::output::{Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::wayland_server::Display;
use super::*;
let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap());
assert!(runtime.starts_with(std::env::temp_dir()));
let mut event_loop = EventLoop::try_new().unwrap();
let config = crate::config::Config::default().runtime_config().unwrap();
let mut state = Ferese::new(&mut event_loop, Display::new().unwrap(), config).unwrap();
let output = Output::new(
"snapshot-test".into(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
output.change_current_state(
Some(smithay::output::Mode {
size: (800, 600).into(),
refresh: 60_000,
}),
Some(smithay::utils::Transform::Normal),
None,
Some((0, 0).into()),
);
state.space.map_output(&output, (0, 0));
state.register_output(&output, "snapshot-test".into());
let original = state.shell_snapshot();
for _ in 0..100 {
assert!(state.shell_snapshot_matches(&original));
}
state
.workspaces
.insert_window(WindowId(99), ferese_layout::Axis::Horizontal, 0.5)
.unwrap();
assert!(!state.shell_snapshot_matches(&original));
let occupied = state.shell_snapshot();
assert!(state.shell_snapshot_matches(&occupied));
let output_id = state.output_ids[&output];
let next = state.workspaces.create_workspace();
state.output_workspaces.assign_workspace(output_id, next).unwrap();
assert!(!state.shell_snapshot_matches(&occupied));
let assigned = state.shell_snapshot();
state.output_workspaces.switch_workspace(output_id, next).unwrap();
assert!(!state.shell_snapshot_matches(&assigned));
let selected = state.shell_snapshot();
assert!(state.shell_snapshot_matches(&selected));
state.unregister_output(&output);
assert!(!state.shell_snapshot_matches(&selected));
}
#[test] #[test]
fn logout_serial_cannot_authorize_a_different_session_query() { fn logout_serial_cannot_authorize_a_different_session_query() {
assert!(super::logout_confirmation_matches(Some(42), Some(9), 42, 9)); assert!(super::logout_confirmation_matches(Some(42), Some(9), 42, 9));
+108
View File
@@ -21,6 +21,7 @@ pub(crate) fn layer_priority(floating: bool, zooming: bool, fullscreen: bool, ab
pub(crate) struct WindowStack { pub(crate) struct WindowStack {
order: Vec<WindowId>, order: Vec<WindowId>,
ranks: HashMap<WindowId, usize>, ranks: HashMap<WindowId, usize>,
revision: u64,
} }
impl WindowStack { impl WindowStack {
@@ -28,12 +29,14 @@ impl WindowStack {
if let std::collections::hash_map::Entry::Vacant(entry) = self.ranks.entry(id) { if let std::collections::hash_map::Entry::Vacant(entry) = self.ranks.entry(id) {
entry.insert(self.order.len()); entry.insert(self.order.len());
self.order.push(id); self.order.push(id);
self.revision = self.revision.wrapping_add(1);
} }
} }
pub fn remove(&mut self, id: WindowId) { pub fn remove(&mut self, id: WindowId) {
if let Some(rank) = self.ranks.remove(&id) { if let Some(rank) = self.ranks.remove(&id) {
self.order.remove(rank); self.order.remove(rank);
self.revision = self.revision.wrapping_add(1);
for (rank, candidate) in self.order.iter().enumerate().skip(rank) { for (rank, candidate) in self.order.iter().enumerate().skip(rank) {
self.ranks.insert(*candidate, rank); self.ranks.insert(*candidate, rank);
} }
@@ -41,6 +44,10 @@ impl WindowStack {
} }
pub fn raise(&mut self, id: WindowId) { pub fn raise(&mut self, id: WindowId) {
if self.order.last() == Some(&id) {
return;
}
self.remove(id); self.remove(id);
self.insert(id); self.insert(id);
} }
@@ -48,12 +55,113 @@ impl WindowStack {
pub fn rank(&self, id: WindowId) -> usize { pub fn rank(&self, id: WindowId) -> usize {
self.ranks.get(&id).copied().unwrap_or(usize::MAX) self.ranks.get(&id).copied().unwrap_or(usize::MAX)
} }
pub fn revision(&self) -> u64 {
self.revision
}
}
/// Desired order survives coordinate-only remaps, which Smithay also raises.
pub(crate) struct StackingCache<W> {
order: Vec<W>,
revision: Option<u64>,
dirty: bool,
remapped: bool,
}
impl<W> Default for StackingCache<W> {
fn default() -> Self {
Self {
order: Vec::new(),
revision: None,
dirty: true,
remapped: true,
}
}
}
impl<W> StackingCache<W> {
pub fn invalidate(&mut self) {
self.dirty = true;
}
pub fn remapped(&mut self) {
self.remapped = true;
}
pub fn needs_rebuild(&self, revision: u64, mapped: usize) -> bool {
self.dirty || self.revision != Some(revision) || self.order.len() != mapped
}
pub fn needs_restore(&self) -> bool {
self.remapped
}
pub fn replace(&mut self, order: Vec<W>, revision: u64) {
self.order = order;
self.revision = Some(revision);
self.dirty = false;
self.remapped = true;
}
pub fn order(&self) -> &[W] {
&self.order
}
pub fn restored(&mut self) {
self.remapped = false;
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn clean_stacking_and_coordinate_remaps_do_not_require_resorting() {
let mut cache = StackingCache::default();
assert!(cache.needs_rebuild(1, 2));
cache.replace(vec!["first", "second"], 1);
cache.restored();
let storage = cache.order().as_ptr();
for _ in 0..100 {
assert!(!cache.needs_rebuild(1, 2));
assert!(!cache.needs_restore());
assert_eq!(cache.order().as_ptr(), storage);
}
cache.remapped();
assert!(!cache.needs_rebuild(1, 2));
assert!(cache.needs_restore());
assert_eq!(cache.order(), ["first", "second"]);
cache.restored();
assert!(cache.needs_rebuild(2, 2));
assert!(cache.needs_rebuild(1, 1));
cache.invalidate();
assert!(cache.needs_rebuild(1, 2));
}
#[test]
fn stack_revision_changes_only_when_membership_or_rank_changes() {
let mut stack = WindowStack::default();
stack.insert(WindowId(1));
let first = stack.revision();
stack.insert(WindowId(1));
stack.raise(WindowId(1));
stack.remove(WindowId(9));
assert_eq!(stack.revision(), first);
stack.insert(WindowId(2));
let second = stack.revision();
assert_ne!(second, first);
stack.raise(WindowId(1));
assert_ne!(stack.revision(), second);
let raised = stack.revision();
stack.remove(WindowId(1));
assert_ne!(stack.revision(), raised);
}
#[test] #[test]
fn cached_ranks_match_reference_order_after_mixed_operations() { fn cached_ranks_match_reference_order_after_mixed_operations() {
let mut stack = WindowStack::default(); let mut stack = WindowStack::default();
+37 -6
View File
@@ -76,7 +76,7 @@ use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconManager;
use window_registry::WindowRegistry; use window_registry::WindowRegistry;
use crate::backends::direct::DirectBackendState; use crate::backends::direct::DirectBackendState;
use crate::config::{Binding, DaemonConfig, InputSettings, OutputProfile, ThemeSettings}; use crate::config::{Binding, BindingSet, DaemonConfig, InputSettings, OutputProfile, ThemeSettings};
use crate::cursor::{NamedCursor, cursor_theme, load_named_cursor}; use crate::cursor::{NamedCursor, cursor_theme, load_named_cursor};
use crate::dimming::DimAnimation; use crate::dimming::DimAnimation;
use crate::gestures::{Swipe, SwipeDirection}; use crate::gestures::{Swipe, SwipeDirection};
@@ -91,7 +91,7 @@ use crate::portal_session::PortalSession;
use crate::portal_shortcuts::PortalShortcuts; use crate::portal_shortcuts::PortalShortcuts;
use crate::session_lock::{IdleSettings, Lock}; use crate::session_lock::{IdleSettings, Lock};
use crate::shell_control::ShellSnapshot; use crate::shell_control::ShellSnapshot;
use crate::stacking::WindowStack; use crate::stacking::{StackingCache, WindowStack};
use crate::wallpaper::{WallpaperConfig, WallpaperState}; use crate::wallpaper::{WallpaperConfig, WallpaperState};
use crate::window_rules::{WindowRule, resolve as resolve_window_rules}; use crate::window_rules::{WindowRule, resolve as resolve_window_rules};
use crate::winit::NestedBackend; use crate::winit::NestedBackend;
@@ -228,8 +228,13 @@ impl WorkspaceSlide {
} }
fn progress(&self) -> f64 { fn progress(&self) -> f64 {
self.progress_at(Duration::ZERO)
}
fn progress_at(&self, delta: Duration) -> f64 {
self.held_progress.unwrap_or_else(|| { self.held_progress.unwrap_or_else(|| {
smoothstep((self.elapsed.as_secs_f64() / WORKSPACE_SLIDE_DURATION.as_secs_f64()).min(1.0)) let elapsed = self.elapsed.saturating_add(delta).min(WORKSPACE_SLIDE_DURATION);
smoothstep(elapsed.as_secs_f64() / WORKSPACE_SLIDE_DURATION.as_secs_f64())
}) })
} }
@@ -260,6 +265,7 @@ impl WorkspaceSlide {
self.items.iter().any(|item| item.workspace == workspace) self.items.iter().any(|item| item.workspace == workspace)
} }
#[cfg(test)]
fn offset(&self, workspace: WorkspaceId, width: f64, height: f64) -> (f64, f64) { fn offset(&self, workspace: WorkspaceId, width: f64, height: f64) -> (f64, f64) {
let progress = self.progress(); let progress = self.progress();
let position = self let position = self
@@ -323,12 +329,15 @@ pub struct Ferese {
pub workspaces: WorkspaceSet, pub workspaces: WorkspaceSet,
pub output_workspaces: OutputWorkspaceMap, pub output_workspaces: OutputWorkspaceMap,
pub(crate) output_ids: HashMap<Output, OutputId>, pub(crate) output_ids: HashMap<Output, OutputId>,
outputs_by_id: HashMap<OutputId, Output>,
pub(crate) output_names: HashMap<OutputId, String>,
output_identity_ids: HashMap<String, OutputId>, output_identity_ids: HashMap<String, OutputId>,
pub(crate) windows: WindowRegistry<Window>, pub(crate) windows: WindowRegistry<Window>,
pub(crate) render: crate::render::RenderResources, pub(crate) render: crate::render::RenderResources,
pub(crate) nested_backend: Option<NestedBackend>, pub(crate) nested_backend: Option<NestedBackend>,
pub(crate) wallpaper: WallpaperState, pub(crate) wallpaper: WallpaperState,
window_stack: WindowStack, window_stack: WindowStack,
stacking_cache: StackingCache<Window>,
floating_above_fullscreen: HashMap<WindowId, WindowId>, floating_above_fullscreen: HashMap<WindowId, WindowId>,
floating_memory: crate::floating::Memory, floating_memory: crate::floating::Memory,
floating_save_worker: crate::floating::SaveWorker, floating_save_worker: crate::floating::SaveWorker,
@@ -341,13 +350,14 @@ pub struct Ferese {
viewport_animations: HashMap<WorkspaceId, AnimatedValue>, viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
focus_swipe: Option<FocusSwipe>, focus_swipe: Option<FocusSwipe>,
workspace_slides: HashMap<OutputId, WorkspaceSlide>, workspace_slides: HashMap<OutputId, WorkspaceSlide>,
workspace_slide_offsets: HashMap<WorkspaceId, (f64, f64)>,
pub focused_window: Option<WindowId>, pub focused_window: Option<WindowId>,
pub(crate) focus_history: ferese_core::FocusHistory, pub(crate) focus_history: ferese_core::FocusHistory,
pub(crate) focus_cycle: Option<ferese_core::FocusCycle>, pub(crate) focus_cycle: Option<ferese_core::FocusCycle>,
column_width_presets: Vec<ColumnWidth>, column_width_presets: Vec<ColumnWidth>,
gap_config: GapConfig, gap_config: GapConfig,
pub(crate) input_settings: InputSettings, pub(crate) input_settings: InputSettings,
pub(crate) bindings: Vec<Binding>, pub(crate) bindings: BindingSet,
workspace_auto_back_and_forth: bool, workspace_auto_back_and_forth: bool,
window_rules: Vec<WindowRule>, window_rules: Vec<WindowRule>,
pub(crate) theme_settings: ThemeSettings, pub(crate) theme_settings: ThemeSettings,
@@ -543,12 +553,15 @@ impl Ferese {
), ),
output_workspaces: OutputWorkspaceMap::default(), output_workspaces: OutputWorkspaceMap::default(),
output_ids: HashMap::new(), output_ids: HashMap::new(),
outputs_by_id: HashMap::new(),
output_names: HashMap::new(),
output_identity_ids: HashMap::new(), output_identity_ids: HashMap::new(),
windows: WindowRegistry::default(), windows: WindowRegistry::default(),
render: Default::default(), render: Default::default(),
nested_backend: None, nested_backend: None,
wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())), wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())),
window_stack: WindowStack::default(), window_stack: WindowStack::default(),
stacking_cache: StackingCache::default(),
floating_above_fullscreen: HashMap::new(), floating_above_fullscreen: HashMap::new(),
floating_memory: crate::floating::Memory::path() floating_memory: crate::floating::Memory::path()
.map(|p| crate::floating::Memory::load(&p)) .map(|p| crate::floating::Memory::load(&p))
@@ -563,13 +576,14 @@ impl Ferese {
viewport_animations: HashMap::new(), viewport_animations: HashMap::new(),
focus_swipe: None, focus_swipe: None,
workspace_slides: HashMap::new(), workspace_slides: HashMap::new(),
workspace_slide_offsets: HashMap::new(),
focused_window: None, focused_window: None,
focus_history: Default::default(), focus_history: Default::default(),
focus_cycle: None, focus_cycle: None,
column_width_presets: config.column_width_presets, column_width_presets: config.column_width_presets,
gap_config: config.gap_config, gap_config: config.gap_config,
input_settings: config.input_settings, input_settings: config.input_settings,
bindings: config.bindings, bindings: config.bindings.into(),
workspace_auto_back_and_forth: config.workspace_auto_back_and_forth, workspace_auto_back_and_forth: config.workspace_auto_back_and_forth,
window_rules: config.window_rules, window_rules: config.window_rules,
theme_settings: config.theme_settings, theme_settings: config.theme_settings,
@@ -707,7 +721,7 @@ impl Ferese {
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
self.gap_config = config.gap_config; self.gap_config = config.gap_config;
self.input_settings = config.input_settings; self.input_settings = config.input_settings;
self.bindings = config.bindings; self.bindings = config.bindings.into();
self.workspace_auto_back_and_forth = config.workspace_auto_back_and_forth; self.workspace_auto_back_and_forth = config.workspace_auto_back_and_forth;
self.portal_shortcuts.reconcile(&self.bindings, &self.input_settings); self.portal_shortcuts.reconcile(&self.bindings, &self.input_settings);
let old_rules = std::mem::replace(&mut self.window_rules, config.window_rules); let old_rules = std::mem::replace(&mut self.window_rules, config.window_rules);
@@ -717,6 +731,7 @@ impl Ferese {
self.animations_enabled = config.animations_enabled; self.animations_enabled = config.animations_enabled;
if !self.animations_enabled { if !self.animations_enabled {
self.workspace_slides.clear(); self.workspace_slides.clear();
self.workspace_slide_offsets.clear();
} }
self.animation_speed = config.animation_speed; self.animation_speed = config.animation_speed;
self.spring_config = config.spring_config; self.spring_config = config.spring_config;
@@ -976,6 +991,22 @@ impl ClientData for ClientState {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[test]
fn sampling_slide_progress_matches_advancing_without_mutating_the_slide() {
let slide = WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Left);
for delta in [Duration::ZERO, Duration::from_millis(8), Duration::from_secs(1)] {
let mut advanced = slide.clone();
advanced.advance(delta);
assert_eq!(slide.progress_at(delta), advanced.progress());
assert_eq!(slide.elapsed, WORKSPACE_SLIDE_FIRST_FRAME);
}
let mut held = slide;
held.held_progress = Some(0.37);
assert_eq!(held.progress_at(Duration::from_secs(1)), 0.37);
}
#[test] #[test]
fn configured_motion_policy_scales_durations_and_reduced_motion_wins() { fn configured_motion_policy_scales_durations_and_reduced_motion_wins() {
for (source, milliseconds) in [ for (source, milliseconds) in [
+235 -52
View File
@@ -20,7 +20,56 @@ impl Ferese {
self.last_animation_tick = Instant::now(); self.last_animation_tick = Instant::now();
} }
fn animations_need_tick(&self) -> bool {
if self.focus_swipe.is_some()
|| !self.workspace_slides.is_empty()
|| !self.dismissing_popups.is_empty()
|| self.render.snapshots().next().is_some()
|| self.overview.needs_tick()
{
return true;
}
let moving = |value: &AnimatedValue| value.current != value.target || value.velocity != 0.0;
if self.viewport_animations.iter().any(|(workspace, viewport)| {
self.output_workspaces
.output_for_workspace(*workspace)
.is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(*workspace))
&& moving(viewport)
}) {
return true;
}
let selected = if self.overview.is_active() {
self.overview.selected()
} else {
self.focused_window
};
self.windows.records().any(|(&id, record)| {
let focus = if selected == Some(id) { 1.0 } else { 0.0 };
let dim = crate::dimming::target(
self.inactive_dim,
self.focused_window,
id,
self.overview.is_presenting(),
);
record_needs_tick(record, focus, dim, || {
self.windows
.window(id)
.is_some_and(|window| self.space.element_location(window).is_some())
})
})
}
pub(super) fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool { pub(super) fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool {
if !self.animations_need_tick() {
self.sync_window_stacking();
return false;
}
let delta = delta.mul_f64(self.animation_speed); let delta = delta.mul_f64(self.animation_speed);
if self if self
.focus_swipe .focus_swipe
@@ -30,21 +79,6 @@ impl Ferese {
self.focus_swipe = None; self.focus_swipe = None;
} }
let visible_workspaces = if self.viewport_animations.is_empty() {
HashSet::new()
} else {
self.space
.outputs()
.filter_map(|output| self.output_ids.get(output))
.filter_map(|output| self.output_workspaces.active_workspace(*output))
.collect::<HashSet<_>>()
};
let windows = self
.space
.elements()
.filter_map(|window| self.windows.ids().get(window).copied().map(|id| (window.clone(), id)))
.collect::<Vec<_>>();
let mut active_animation = false; let mut active_animation = false;
let mut completed_slides = Vec::new(); let mut completed_slides = Vec::new();
for (output, slide) in &mut self.workspace_slides { for (output, slide) in &mut self.workspace_slides {
@@ -102,9 +136,12 @@ impl Ferese {
dim_changed |= previous != focus.current; dim_changed |= previous != focus.current;
} }
for (_, id) in &windows { for window in self.space.elements() {
let target = crate::dimming::target(dim_settings, self.focused_window, *id, self.overview.is_presenting()); let Some(id) = self.windows.ids().get(window).copied() else {
let Some(record) = self.windows.record_mut(*id) else { continue;
};
let target = crate::dimming::target(dim_settings, self.focused_window, id, self.overview.is_presenting());
let Some(record) = self.windows.record_mut(id) else {
continue; continue;
}; };
@@ -184,7 +221,11 @@ impl Ferese {
// Keep scheduling frames while waiting, so the deadline cannot stall. // Keep scheduling frames while waiting, so the deadline cannot stall.
active_animation |= !blocked_workspaces.is_empty(); active_animation |= !blocked_workspaces.is_empty();
for (workspace, viewport) in &mut self.viewport_animations { for (workspace, viewport) in &mut self.viewport_animations {
if !visible_workspaces.contains(workspace) { if !self
.output_workspaces
.output_for_workspace(*workspace)
.is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(*workspace))
{
continue; continue;
} }
@@ -202,8 +243,13 @@ impl Ferese {
} }
} }
let mut settled_coupled_widths = Vec::new(); let mut remaps = Vec::new();
for (window, id) in windows {
for window in self.space.elements() {
let Some(id) = self.windows.ids().get(window).copied() else {
continue;
};
if self if self
.workspaces .workspaces
.workspace_for_window(id) .workspace_for_window(id)
@@ -229,6 +275,11 @@ impl Ferese {
geometry.visual.velocity.width = 0.0; geometry.visual.velocity.width = 0.0;
} }
active_animation |= geometry.advance(delta, self.spring_config, self.animations_enabled); active_animation |= geometry.advance(delta, self.spring_config, self.animations_enabled);
if zooming != geometry.is_zooming() {
self.stacking_cache.invalidate();
}
if let Some(target) = coupled_target { if let Some(target) = coupled_target {
geometry.visual.target.width = target; geometry.visual.target.width = target;
} }
@@ -262,7 +313,7 @@ impl Ferese {
geometry.visual.current.width = width.current; geometry.visual.current.width = width.current;
geometry.visual.velocity.width = width.velocity; geometry.visual.velocity.width = width.velocity;
if !width_active { if !width_active {
settled_coupled_widths.push(id); record.coupled_width = None;
} }
} }
@@ -287,12 +338,18 @@ impl Ferese {
} }
let visual = geometry.visual.current; let visual = geometry.visual.current;
self.space let location = (visual.x.round() as i32, visual.y.round() as i32).into();
.map_element(window, (visual.x.round() as i32, visual.y.round() as i32), false);
if self.space.element_location(window) != Some(location) {
remaps.push((id, location));
}
} }
for id in settled_coupled_widths { for (id, location) in remaps {
self.windows.update(id, |record| record.coupled_width = None); if let Some(window) = self.windows.window(id) {
self.space.map_element(window.clone(), location, false);
self.stacking_cache.remapped();
}
} }
if !completed_slides.is_empty() { if !completed_slides.is_empty() {
@@ -320,6 +377,7 @@ impl Ferese {
active_animation |= self active_animation |= self
.overview .overview
.advance(delta, self.spring_config, self.animations_enabled); .advance(delta, self.spring_config, self.animations_enabled);
self.refresh_workspace_slide_offsets();
self.sync_window_stacking(); self.sync_window_stacking();
if active_animation || dim_changed { if active_animation || dim_changed {
@@ -337,48 +395,173 @@ impl Ferese {
} }
pub(crate) fn workspace_slide_offset(&self, window: WindowId) -> (f64, f64) { pub(crate) fn workspace_slide_offset(&self, window: WindowId) -> (f64, f64) {
self.workspace_slide_offset_at(window, Duration::ZERO) self.workspaces
.workspace_for_window(window)
.and_then(|workspace| self.workspace_slide_offsets.get(&workspace).copied())
.unwrap_or_default()
} }
pub(super) fn workspace_slide_offset_at(&self, window: WindowId, delta: Duration) -> (f64, f64) { pub(super) fn sample_workspace_slide_offsets(
let Some(workspace) = self.workspaces.workspace_for_window(window) else { &self,
return (0.0, 0.0); output: Option<OutputId>,
}; delta: Duration,
) -> HashMap<WorkspaceId, (f64, f64)> {
let mut offsets = HashMap::new();
self.fill_workspace_slide_offsets(output, delta, &mut offsets);
offsets
}
let Some(output_id) = self pub(super) fn refresh_workspace_slide_offsets(&mut self) {
.workspace_slides // Reuse the input snapshot's allocation. Predicted render snapshots
.iter() // remain separate and never change hit-testing or gesture state.
.find_map(|(output, slide)| slide.contains(workspace).then_some(*output)) let mut offsets = std::mem::take(&mut self.workspace_slide_offsets);
.or_else(|| self.output_workspaces.output_for_workspace(workspace)) offsets.clear();
else { self.fill_workspace_slide_offsets(None, Duration::ZERO, &mut offsets);
return (0.0, 0.0); self.workspace_slide_offsets = offsets;
}; }
let Some(slide) = self.workspace_slides.get(&output_id) else { fn fill_workspace_slide_offsets(
return (0.0, 0.0); &self,
}; output: Option<OutputId>,
delta: Duration,
offsets: &mut HashMap<WorkspaceId, (f64, f64)>,
) {
for (output_id, slide) in &self.workspace_slides {
if output.is_some_and(|output| output != *output_id) {
continue;
}
let Some(size) = self let Some(size) = self
.output_ids .outputs_by_id
.iter() .get(output_id)
.find(|(_, id)| **id == output_id) .and_then(|output| self.space.output_geometry(output))
.and_then(|(output, _)| self.space.output_geometry(output))
.map(|geometry| geometry.size) .map(|geometry| geometry.size)
else { else {
return (0.0, 0.0); continue;
}; };
let progress = slide.progress_at(delta);
let mut slide = slide.clone(); for item in &slide.items {
if slide.held_progress.is_none() && !delta.is_zero() { let position = item.start.between(item.target, progress);
slide.advance(delta); offsets.insert(
item.workspace,
(position.x * f64::from(size.w), position.y * f64::from(size.h)),
);
}
} }
slide.offset(workspace, f64::from(size.w), f64::from(size.h))
} }
pub(crate) fn cancel_workspace_slides(&mut self) -> bool { pub(crate) fn cancel_workspace_slides(&mut self) -> bool {
let active = !self.workspace_slides.is_empty(); let active = !self.workspace_slides.is_empty();
self.workspace_slides.clear(); self.workspace_slides.clear();
self.workspace_slide_offsets.clear();
active active
} }
} }
// Inspect current targets, not the previous frame's activity. Target changes
// and client waits must wake a desktop that was settled on its last tick.
fn record_needs_tick(
record: &super::window_registry::WindowRecord,
focus: f64,
dim: f64,
mapped: impl FnOnce() -> bool,
) -> bool {
if record.resize.is_some()
|| record.closing.as_ref().is_some_and(|close| !close.close_sent)
|| record.focus.as_ref().is_none_or(|motion| motion.needs_update(focus))
{
return true;
}
let moving = |value: &AnimatedValue| value.current != value.target || value.velocity != 0.0;
let pending = record.dimming.as_ref().is_none_or(|motion| motion.needs_update(dim))
|| record.world_x.as_ref().is_some_and(|(_, world)| moving(world))
|| record.coupled_width.is_some()
|| record.geometry.is_some_and(|geometry| {
geometry.is_zooming()
|| geometry.presentation_changed
|| geometry.client.waiting_for_commit()
|| geometry.visual.current != geometry.visual.target
|| geometry.visual.velocity != ferese_animation::RectVelocity::default()
|| (!record.natural_floating_pending
&& geometry.client.last_configured_size != Some(ClientSize::from_rect(geometry.logical)))
});
// Hidden geometry and dimming do not advance in the full path.
// A stale hidden target must not keep waking the whole desktop.
pending && mapped()
}
#[cfg(test)]
mod tests {
use super::super::window_registry::WindowRecord;
use super::*;
fn settled() -> WindowRecord {
let rect = Rect::new(0.0, 0.0, 400.0, 300.0);
let mut geometry = WindowGeometry::new(rect, Some(ClientSize::from_rect(rect)));
geometry.presentation_changed = false;
WindowRecord {
geometry: Some(geometry),
focus: Some(DimAnimation::new(1.0)),
dimming: Some(DimAnimation::new(0.0)),
..Default::default()
}
}
#[test]
fn idle_check_skips_mapping_lookup_but_new_focus_and_dim_targets_wake_it() {
let record = settled();
for _ in 0..100 {
assert!(!record_needs_tick(&record, 1.0, 0.0, || panic!(
"settled record needs no mapping lookup"
)));
}
assert!(record_needs_tick(&record, 0.0, 0.0, || true));
assert!(record_needs_tick(&record, 1.0, 0.15, || true));
assert!(!record_needs_tick(&record, 1.0, 0.15, || false));
}
#[test]
fn geometry_motion_client_waits_and_resize_barriers_cannot_be_skipped() {
let mut record = settled();
record
.geometry
.as_mut()
.unwrap()
.visual
.set_target(Rect::new(10.0, 0.0, 400.0, 300.0));
assert!(record_needs_tick(&record, 1.0, 0.0, || true));
assert!(!record_needs_tick(&record, 1.0, 0.0, || false));
let mut record = settled();
record.geometry.as_mut().unwrap().client.request_size(
ClientSize {
width: 500,
height: 300,
},
Duration::ZERO,
);
assert!(record_needs_tick(&record, 1.0, 0.0, || true));
record.resize = Some(crate::resize_transaction::ResizeTransaction::new(
1.into(),
Duration::ZERO,
));
assert!(record_needs_tick(&record, 1.0, 0.0, || false));
}
#[test]
fn close_requests_and_coupled_width_cleanup_still_get_their_final_tick() {
let mut record = settled();
record.closing = Some(ClosingAnimation::default());
assert!(record_needs_tick(&record, 1.0, 0.0, || false));
record.closing.as_mut().unwrap().close_sent = true;
assert!(!record_needs_tick(&record, 1.0, 0.0, || true));
record.coupled_width = Some((WorkspaceId(1), AnimatedValue::new(400.0)));
assert!(record_needs_tick(&record, 1.0, 0.0, || true));
}
}
+10 -19
View File
@@ -17,12 +17,7 @@ impl Ferese {
Some(WindowPlacement::Floating { rect }) => rect, Some(WindowPlacement::Floating { rect }) => rect,
None => return, None => return,
}; };
if let Some(handle) = self if let Some(handle) = self.windows.window(window).cloned() {
.windows
.ids()
.iter()
.find_map(|(handle, id)| (*id == window).then_some(handle.clone()))
{
if matches!( if matches!(
self.workspaces.placement(window), self.workspaces.placement(window),
Some(WindowPlacement::Floating { .. }) Some(WindowPlacement::Floating { .. })
@@ -178,6 +173,10 @@ impl Ferese {
.geometry .geometry
.get_or_insert_with(|| WindowGeometry::new(rect, client_size(window))); .get_or_insert_with(|| WindowGeometry::new(rect, client_size(window)));
if geometry.is_zooming() {
self.stacking_cache.invalidate();
}
if let Some(size) = geometry.follow_pointer(rect, self.start_time.elapsed()) if let Some(size) = geometry.follow_pointer(rect, self.start_time.elapsed())
&& let Some(toplevel) = window.toplevel() && let Some(toplevel) = window.toplevel()
{ {
@@ -188,7 +187,7 @@ impl Ferese {
} }
self.windows.update(id, |record| record.world_x = None); self.windows.update(id, |record| record.world_x = None);
self.windows.update(id, |record| record.coupled_width = None); self.windows.update(id, |record| record.coupled_width = None);
self.space.map_element(window.clone(), location, false); self.map_window_geometry(window.clone(), location);
self.sync_window_stacking(); self.sync_window_stacking();
// Move/resize/cancel callbacks run with Smithay's pointer mutex held. // Move/resize/cancel callbacks run with Smithay's pointer mutex held.
// Cursor rendering reads current_location(), which would lock it again. // Cursor rendering reads current_location(), which would lock it again.
@@ -269,14 +268,7 @@ impl Ferese {
let (app, parent) = Self::floating_metadata(window); let (app, parent) = Self::floating_metadata(window);
let parent_rect = parent let parent_rect = parent
.as_ref() .as_ref()
.and_then(|surface| { .and_then(|surface| self.windows.id_for_surface(surface))
self.windows.ids().iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|top| top.wl_surface() == surface)
.then_some(*id)
})
})
.and_then(|parent| self.presented_window_rect(parent)) .and_then(|parent| self.presented_window_rect(parent))
.and_then(|rect| intersection(rect, work)); .and_then(|rect| intersection(rect, work));
let dialog = crate::window_rules::is_native_dialog(app.as_deref()); let dialog = crate::window_rules::is_native_dialog(app.as_deref());
@@ -420,10 +412,9 @@ impl Ferese {
.workspaces .workspaces
.workspace_for_window(window) .workspace_for_window(window)
.and_then(|workspace| self.output_workspaces.output_for_workspace(workspace)); .and_then(|workspace| self.output_workspaces.output_for_workspace(workspace));
self.output_ids output_id
.iter() .and_then(|id| self.outputs_by_id.get(&id))
.find(|(_, id)| Some(**id) == output_id) .and_then(|output| self.output_bounds_for(output))
.and_then(|(output, _)| self.output_bounds_for(output))
.or_else(|| self.output_bounds()) .or_else(|| self.output_bounds())
} }
+2 -2
View File
@@ -49,7 +49,7 @@ impl Ferese {
let available = self.focus_candidates(); let available = self.focus_candidates();
if !preview { if !preview {
self.cancel_focus_cycle(); self.cancel_focus_cycle();
let order = self.focus_history.candidates(self.focused_window, available.clone()); let order = self.focus_history.candidates(self.focused_window, &available);
let mut cycle = ferese_core::FocusCycle::new(order, self.focused_window); let mut cycle = ferese_core::FocusCycle::new(order, self.focused_window);
if let Some(id) = cycle.advance(reverse, &available) { if let Some(id) = cycle.advance(reverse, &available) {
self.activate_managed_window(id); self.activate_managed_window(id);
@@ -57,7 +57,7 @@ impl Ferese {
return; return;
} }
if self.focus_cycle.is_none() { if self.focus_cycle.is_none() {
let order = self.focus_history.candidates(self.focused_window, available.clone()); let order = self.focus_history.candidates(self.focused_window, &available);
if order.len() < 2 { if order.len() < 2 {
return; return;
} }
+1 -4
View File
@@ -166,10 +166,7 @@ impl Ferese {
}; };
if overview_active { if overview_active {
let mut candidates = self.windows.ids().keys().cloned().collect::<Vec<_>>(); self.windows.overview_windows().find_map(hit)
// Match Overview's front-to-back render order during overlapping motion.
candidates.sort_by_key(|window| std::cmp::Reverse(self.windows.ids().get(window).map_or(0, |id| id.0)));
candidates.iter().find_map(hit)
} else { } else {
self.space.elements().rev().find_map(hit) self.space.elements().rev().find_map(hit)
} }
+35 -7
View File
@@ -59,6 +59,7 @@ impl Ferese {
// Consume idle time before setting new targets; it must not become a // Consume idle time before setting new targets; it must not become a
// large first animation step after a keypress on an idle desktop. // large first animation step after a keypress on an idle desktop.
self.advance_animations(Instant::now()); self.advance_animations(Instant::now());
self.stacking_cache.invalidate();
self.arrange_layers(); self.arrange_layers();
let mut previous_scrolling_world_x = self.windows.take_world_positions(); let mut previous_scrolling_world_x = self.windows.take_world_positions();
let pending_column_width_cycles = self.windows.take_column_width_requests(); let pending_column_width_cycles = self.windows.take_column_width_requests();
@@ -303,7 +304,7 @@ impl Ferese {
let location = (visual.x.round() as i32, visual.y.round() as i32); let location = (visual.x.round() as i32, visual.y.round() as i32);
self.space.map_element(window.clone(), location, false); self.map_window_geometry(window.clone(), location.into());
if let Some(toplevel) = window.toplevel() { if let Some(toplevel) = window.toplevel() {
let state_changed = toplevel.with_pending_state(|state| { let state_changed = toplevel.with_pending_state(|state| {
if natural_pending { if natural_pending {
@@ -372,16 +373,29 @@ impl Ferese {
} }
self.space.raise_element(window, activate); self.space.raise_element(window, activate);
self.stacking_cache.remapped();
self.sync_window_stacking(); self.sync_window_stacking();
} }
pub(crate) fn invalidate_window_stacking(&mut self) {
self.stacking_cache.invalidate();
}
pub(crate) fn sync_window_stacking(&mut self) { pub(crate) fn sync_window_stacking(&mut self) {
let revision = self.window_stack.revision();
let rebuild = self.stacking_cache.needs_rebuild(revision, self.space.elements().len());
if !rebuild && !self.stacking_cache.needs_restore() {
return;
}
if rebuild {
let mut windows = self.space.elements().cloned().collect::<Vec<_>>(); let mut windows = self.space.elements().cloned().collect::<Vec<_>>();
windows.sort_by_key(|window| { windows.sort_by_key(|window| {
let id = self.windows.ids().get(window).copied(); let id = self.windows.ids().get(window).copied();
let geometry = id.and_then(|id| self.windows.geometry(&id)); let geometry = id.and_then(|id| self.windows.geometry(&id));
let floating = let floating = id
id.is_some_and(|id| matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }))); .is_some_and(|id| matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. })));
let priority = crate::stacking::layer_priority( let priority = crate::stacking::layer_priority(
floating, floating,
geometry.is_some_and(|geometry| geometry.is_zooming()), geometry.is_some_and(|geometry| geometry.is_zooming()),
@@ -398,13 +412,27 @@ impl Ferese {
); );
(priority, id.map_or(usize::MAX, |id| self.window_stack.rank(id))) (priority, id.map_or(usize::MAX, |id| self.window_stack.rank(id)))
}); });
if stacking_order_settled(self.space.elements(), &windows, |window| window.z_index()) { self.stacking_cache.replace(windows, revision);
}
if !stacking_order_settled(self.space.elements(), self.stacking_cache.order(), |window| {
window.z_index()
}) {
for window in self.stacking_cache.order() {
self.space.raise_element(window, false);
}
}
self.stacking_cache.restored();
}
pub(crate) fn map_window_geometry(&mut self, window: Window, location: Point<i32, Logical>) {
if self.space.element_location(&window) == Some(location) {
return; return;
} }
for window in windows { self.space.map_element(window, location, false);
self.space.raise_element(&window, false); self.stacking_cache.remapped();
}
} }
pub(super) fn tiled_layout(&mut self, bounds: Rect) -> Result<LayoutResult, ferese_layout::LayoutError> { pub(super) fn tiled_layout(&mut self, bounds: Rect) -> Result<LayoutResult, ferese_layout::LayoutError> {
+8 -20
View File
@@ -139,12 +139,7 @@ impl Ferese {
{ {
tracing::warn!(%error, ?current, ?next, "failed to slide swipe focus"); tracing::warn!(%error, ?current, ?next, "failed to slide swipe focus");
} }
let Some(window) = self let Some(window) = self.windows.window(next).cloned() else {
.windows
.ids()
.iter()
.find_map(|(window, id)| (*id == next).then(|| window.clone()))
else {
return; return;
}; };
let Some(surface) = window.toplevel().map(|toplevel| toplevel.wl_surface().clone()) else { let Some(surface) = window.toplevel().map(|toplevel| toplevel.wl_surface().clone()) else {
@@ -306,12 +301,7 @@ impl Ferese {
} }
pub(crate) fn activate_managed_window(&mut self, id: WindowId) -> bool { pub(crate) fn activate_managed_window(&mut self, id: WindowId) -> bool {
let Some(window) = self let Some(window) = self.windows.window(id).cloned() else {
.windows
.ids()
.iter()
.find_map(|(window, window_id)| (*window_id == id).then(|| window.clone()))
else {
return false; return false;
}; };
if !activate_window_workspace(&mut self.workspaces, &mut self.output_workspaces, id) { if !activate_window_workspace(&mut self.workspaces, &mut self.output_workspaces, id) {
@@ -441,6 +431,7 @@ impl Ferese {
.get_mut(&output) .get_mut(&output)
.expect("gesture output exists") .expect("gesture output exists")
.held_progress = Some(progress); .held_progress = Some(progress);
self.refresh_workspace_slide_offsets();
self.backdrop_generation = self.backdrop_generation.wrapping_add(1); self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
self.cursor_redraw_pending = true; self.cursor_redraw_pending = true;
return; return;
@@ -555,14 +546,11 @@ impl Ferese {
self.focus_lock_surface(); self.focus_lock_surface();
return; return;
} }
let surface = self.focused_window.and_then(|focused| { let surface = self
self.windows.ids().iter().find_map(|(window, id)| { .focused_window
(*id == focused) .and_then(|focused| self.windows.window(focused))
.then(|| window.toplevel()) .and_then(|window| window.toplevel())
.flatten() .map(|toplevel| toplevel.wl_surface().clone());
.map(|toplevel| toplevel.wl_surface().clone())
})
});
self.seat.get_keyboard().expect("seat has a keyboard").set_focus( self.seat.get_keyboard().expect("seat has a keyboard").set_focus(
self, self,
+5 -3
View File
@@ -29,6 +29,8 @@ impl Ferese {
match registration { match registration {
Ok(_) => { Ok(_) => {
self.output_ids.insert(output.clone(), output_id); self.output_ids.insert(output.clone(), output_id);
self.outputs_by_id.insert(output_id, output.clone());
self.output_names.insert(output_id, output.name());
self.restore_output_focus(); self.restore_output_focus();
let visible = self.visible_workspace_ids(); let visible = self.visible_workspace_ids();
self.reconcile_workspaces(&visible); self.reconcile_workspaces(&visible);
@@ -45,6 +47,8 @@ impl Ferese {
return; return;
}; };
self.outputs_by_id.remove(&output_id);
self.output_names.remove(&output_id);
self.workspace_slides.remove(&output_id); self.workspace_slides.remove(&output_id);
self.render.remove_output(output_id); self.render.remove_output(output_id);
self.pending_screencopies.retain(|capture| { self.pending_screencopies.retain(|capture| {
@@ -113,9 +117,7 @@ impl Ferese {
pub(crate) fn focused_output(&self) -> Option<&Output> { pub(crate) fn focused_output(&self) -> Option<&Output> {
let focused = self.output_workspaces.focused_output()?; let focused = self.output_workspaces.focused_output()?;
self.output_ids self.outputs_by_id.get(&focused)
.iter()
.find_map(|(output, id)| (*id == focused).then_some(output))
} }
pub(crate) fn restore_output_focus(&mut self) { pub(crate) fn restore_output_focus(&mut self) {
+153 -15
View File
@@ -5,6 +5,8 @@ use super::*;
pub(crate) struct FrameScene { pub(crate) struct FrameScene {
pub windows: HashMap<WindowId, WindowFrame>, pub windows: HashMap<WindowId, WindowFrame>,
pub overview: crate::overview::OverviewMotion, pub overview: crate::overview::OverviewMotion,
// Plane selection and scheduling reuse the activity sampled with this scene.
pub animating: bool,
} }
pub(crate) struct WindowFrame { pub(crate) struct WindowFrame {
@@ -17,6 +19,31 @@ pub(crate) struct WindowFrame {
} }
impl Ferese { impl Ferese {
fn output_window_records<'a>(
&'a self,
output: &'a Output,
block_resizes: bool,
) -> impl Iterator<Item = (WindowId, &'a super::window_registry::WindowRecord, bool)> + 'a {
self.output_id(output)
.into_iter()
.flat_map(move |output| self.output_workspaces.assigned_workspaces(output))
.filter_map(move |id| self.workspaces.workspace(id))
.flat_map(move |workspace| {
let blocked = block_resizes
&& workspace
.layout
.window_ids()
.chain(workspace.floating.iter().copied())
.any(|id| self.windows.record(id).is_some_and(|record| record.resize.is_some()));
workspace
.layout
.window_ids()
.chain(workspace.floating.iter().copied())
.filter_map(move |id| Some((id, self.windows.record(id)?, blocked)))
})
}
pub(crate) fn output_has_animations(&self, output: &Output) -> bool { pub(crate) fn output_has_animations(&self, output: &Output) -> bool {
if self.overview.is_animating(self.spring_config) || !self.dismissing_popups.is_empty() { if self.overview.is_animating(self.spring_config) || !self.dismissing_popups.is_empty() {
return true; return true;
@@ -30,17 +57,17 @@ impl Ferese {
return true; return true;
} }
self.windows.records().any(|(id, record)| { self.output_window_records(output, false).any(|(id, record, _)| {
let Some(mut geometry) = record.geometry else { let Some(mut geometry) = record.geometry else {
return false; return false;
}; };
if !self.window_belongs_to_output(*id, output) { if !self.window_belongs_to_output(id, output) {
return false; return false;
} }
if record.resize.is_some() if record.resize.is_some()
|| self.render.snapshot(id).is_some() || self.render.snapshot(&id).is_some()
|| record.closing.as_ref().is_some_and(|close| !close.close_sent) || record.closing.as_ref().is_some_and(|close| !close.close_sent)
|| record.focus.as_ref().is_some_and(|focus| focus.is_animating()) || record.focus.as_ref().is_some_and(|focus| focus.is_animating())
|| record.dimming.as_ref().is_some_and(|dim| dim.is_animating()) || record.dimming.as_ref().is_some_and(|dim| dim.is_animating())
@@ -52,7 +79,7 @@ impl Ferese {
return true; return true;
} }
let Some(workspace) = self.workspaces.workspace_for_window(*id) else { let Some(workspace) = self.workspaces.workspace_for_window(id) else {
return false; return false;
}; };
@@ -71,28 +98,28 @@ impl Ferese {
} }
pub(crate) fn sample_frame(&self, output: &Output, horizon: Duration) -> FrameScene { pub(crate) fn sample_frame(&self, output: &Output, horizon: Duration) -> FrameScene {
let delta = if self.animations_enabled && self.output_has_animations(output) { let animating = self.output_has_animations(output);
let delta = if self.animations_enabled && animating {
horizon.min(Duration::from_millis(100)).mul_f64(self.animation_speed) horizon.min(Duration::from_millis(100)).mul_f64(self.animation_speed)
} else { } else {
Duration::ZERO Duration::ZERO
}; };
let overview = self.overview.sample(delta, self.spring_config); let overview = self.overview.sample(delta, self.spring_config);
let blocked = self let predicted_slide_offsets =
.windows (!delta.is_zero()).then(|| self.sample_workspace_slide_offsets(self.output_id(output), delta));
.resizing() let slide_offsets = predicted_slide_offsets
.filter_map(|id| self.workspaces.workspace_for_window(*id)) .as_ref()
.collect::<HashSet<_>>(); .unwrap_or(&self.workspace_slide_offsets);
let mut windows = HashMap::new(); let mut windows = HashMap::new();
for (&id, record) in self.windows.records() { for (id, record, blocked) in self.output_window_records(output, !delta.is_zero()) {
let Some(original) = record.geometry else { continue }; let Some(original) = record.geometry else { continue };
if !self.window_belongs_to_output(id, output) { if !self.window_belongs_to_output(id, output) {
continue; continue;
} }
let workspace = self.workspaces.workspace_for_window(id);
let world = record.world_x.as_ref().and_then(|(workspace, world)| { let world = record.world_x.as_ref().and_then(|(workspace, world)| {
let viewport = self.viewport_animations.get(workspace)?; let viewport = self.viewport_animations.get(workspace)?;
let held = self let held = self
@@ -103,7 +130,7 @@ impl Ferese {
Some((*world, *viewport, held)) Some((*world, *viewport, held))
}); });
let width = record.coupled_width.as_ref().map(|(_, width)| *width); let width = record.coupled_width.as_ref().map(|(_, width)| *width);
let geometry = if delta.is_zero() || workspace.is_some_and(|workspace| blocked.contains(&workspace)) { let geometry = if delta.is_zero() || blocked {
original original
} else { } else {
predict_geometry( predict_geometry(
@@ -118,7 +145,11 @@ impl Ferese {
let mut rect = overview.presented_rect(id, geometry.visual.current); let mut rect = overview.presented_rect(id, geometry.visual.current);
if !overview.is_presenting() { if !overview.is_presenting() {
let (x, y) = self.workspace_slide_offset_at(id, delta); let (x, y) = self
.workspaces
.workspace_for_window(id)
.and_then(|workspace| slide_offsets.get(&workspace).copied())
.unwrap_or_default();
rect.x += x; rect.x += x;
rect.y += y; rect.y += y;
} }
@@ -160,7 +191,11 @@ impl Ferese {
); );
} }
FrameScene { windows, overview } FrameScene {
windows,
overview,
animating,
}
} }
} }
@@ -259,12 +294,109 @@ mod tests {
{ {
let forecast = state.sample_frame(&output, horizon); let forecast = state.sample_frame(&output, horizon);
assert!(forecast.windows[&id].geometry.visual.current.x > geometry.visual.current.x); assert!(forecast.windows[&id].geometry.visual.current.x > geometry.visual.current.x);
assert!(forecast.animating);
} }
assert_eq!(*state.windows.geometry(&id).unwrap(), geometry); assert_eq!(*state.windows.geometry(&id).unwrap(), geometry);
assert_eq!(state.last_animation_tick, tick); assert_eq!(state.last_animation_tick, tick);
} }
// Sampling one output must not visit or include another output's records.
let other = Output::new(
"other-forecast".into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
other.change_current_state(
Some(smithay::output::Mode {
size: (1280, 720).into(),
refresh: 240_000,
}),
Some(smithay::utils::Transform::Normal),
None,
Some((1920, 0).into()),
);
state.space.map_output(&other, (1920, 0));
state.register_output(&other, "other-forecast".into());
let other_workspace = state
.output_workspaces
.active_workspace(state.output_id(&other).unwrap())
.unwrap();
let other_id = WindowId(2);
state
.workspaces
.insert_floating_window(other_id, other_workspace, geometry.visual.current, false)
.unwrap();
state.windows.records.insert(
other_id,
super::super::window_registry::WindowRecord {
geometry: Some(geometry),
..Default::default()
},
);
assert_eq!(
state
.output_window_records(&output, false)
.map(|(id, _, _)| id)
.collect::<Vec<_>>(),
vec![id]
);
assert_eq!(
state
.output_window_records(&other, false)
.map(|(id, _, _)| id)
.collect::<Vec<_>>(),
vec![other_id]
);
assert!(
!state
.sample_frame(&output, Duration::from_millis(8))
.windows
.contains_key(&other_id)
);
assert!(
!state
.sample_frame(&other, Duration::from_millis(4))
.windows
.contains_key(&id)
);
let output_id = state.output_id(&output).unwrap();
let from = state.workspaces.workspace_for_window(id).unwrap();
let to = state.workspaces.create_workspace();
state.output_workspaces.assign_workspace(output_id, to).unwrap();
let mut slide = WorkspaceSlide::new(None, from, to, SwipeDirection::Left);
slide.held_progress = Some(0.25);
state.workspace_slides.insert(output_id, slide);
state.refresh_workspace_slide_offsets();
let offsets = state.workspace_slide_offsets.clone();
assert_eq!(offsets[&from].0, -480.0);
assert_eq!(offsets[&to].0, 1440.0);
assert_eq!(
state.sample_frame(&output, Duration::from_millis(8)).windows[&id]
.rect
.x,
state.sample_frame(&output, Duration::from_millis(8)).windows[&id]
.geometry
.visual
.current
.x
- 480.0
);
assert_eq!(state.workspace_slide_offsets, offsets);
assert!(
state
.sample_workspace_slide_offsets(state.output_id(&other), Duration::from_millis(8))
.is_empty()
);
state.cancel_workspace_slides();
assert!(state.workspace_slide_offsets.is_empty());
// A prediction must neither advance nor expire a client resize barrier. // A prediction must neither advance nor expire a client resize barrier.
state.windows.set_transaction( state.windows.set_transaction(
id, id,
@@ -278,6 +410,12 @@ mod tests {
state.animations_enabled = false; state.animations_enabled = false;
let forecast = state.sample_frame(&output, Duration::from_millis(16)); let forecast = state.sample_frame(&output, Duration::from_millis(16));
assert_eq!(forecast.windows[&id].geometry, geometry); assert_eq!(forecast.windows[&id].geometry, geometry);
let settled = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None);
state.windows.set_geometry(id, settled);
state.animations_enabled = true;
assert!(!state.sample_frame(&output, Duration::ZERO).animating);
assert!(state.sample_frame(&other, Duration::ZERO).animating);
} }
#[test] #[test]
+141 -6
View File
@@ -4,6 +4,10 @@ use std::hash::Hash;
use ferese_animation::{AnimatedValue, WindowGeometry}; use ferese_animation::{AnimatedValue, WindowGeometry};
use ferese_core::WorkspaceId; use ferese_core::WorkspaceId;
use ferese_layout::{ColumnWidth, Rect, WindowId}; use ferese_layout::{ColumnWidth, Rect, WindowId};
use smithay::desktop::Window;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
use crate::resize_transaction::ResizeTransaction; use crate::resize_transaction::ResizeTransaction;
@@ -12,6 +16,10 @@ use crate::resize_transaction::ResizeTransaction;
#[derive(Default)] #[derive(Default)]
pub(crate) struct WindowRecord { pub(crate) struct WindowRecord {
pub geometry: Option<WindowGeometry>, pub geometry: Option<WindowGeometry>,
pub surface: Option<WlSurface>,
pub app_id: String,
pub title: String,
pub metadata_revision: u64,
pub resize: Option<ResizeTransaction>, pub resize: Option<ResizeTransaction>,
pub focus: Option<crate::dimming::DimAnimation>, pub focus: Option<crate::dimming::DimAnimation>,
pub dimming: Option<crate::dimming::DimAnimation>, pub dimming: Option<crate::dimming::DimAnimation>,
@@ -32,7 +40,11 @@ pub(crate) struct WindowRecord {
/// handle always updates its record as well. IDs are never reused on remapping. /// handle always updates its record as well. IDs are never reused on remapping.
pub(crate) struct WindowRegistry<W> { pub(crate) struct WindowRegistry<W> {
ids: HashMap<W, WindowId>, ids: HashMap<W, WindowId>,
by_id: HashMap<WindowId, W>,
surface_ids: HashMap<WlSurface, WindowId>,
pub(super) records: HashMap<WindowId, WindowRecord>, pub(super) records: HashMap<WindowId, WindowRecord>,
// Ascending IDs; overview reverses this for front-to-back traversal.
overview_order: Vec<WindowId>,
next_id: u64, next_id: u64,
} }
@@ -40,7 +52,10 @@ impl<W> Default for WindowRegistry<W> {
fn default() -> Self { fn default() -> Self {
Self { Self {
ids: HashMap::new(), ids: HashMap::new(),
by_id: HashMap::new(),
surface_ids: HashMap::new(),
records: HashMap::new(), records: HashMap::new(),
overview_order: Vec::new(),
next_id: 1, next_id: 1,
} }
} }
@@ -54,10 +69,16 @@ impl<W: Eq + Hash> WindowRegistry<W> {
id id
} }
pub fn register(&mut self, window: W, id: WindowId) { pub fn register(&mut self, window: W, id: WindowId)
where
W: Clone,
{
assert!(!self.ids.contains_key(&window), "window already registered"); assert!(!self.ids.contains_key(&window), "window already registered");
assert!(!self.records.contains_key(&id), "window ID already registered"); assert!(!self.records.contains_key(&id), "window ID already registered");
let index = self.overview_order.partition_point(|existing| existing.0 < id.0);
self.overview_order.insert(index, id);
self.by_id.insert(id, window.clone());
self.ids.insert(window, id); self.ids.insert(window, id);
self.records.insert(id, WindowRecord::default()); self.records.insert(id, WindowRecord::default());
} }
@@ -66,6 +87,24 @@ impl<W: Eq + Hash> WindowRegistry<W> {
&self.ids &self.ids
} }
/// Overview render and hit order, with the newest ID in front. Normal
/// stacking changes do not affect this independent presentation order.
pub fn overview_windows(&self) -> impl DoubleEndedIterator<Item = &W> {
self.overview_order.iter().rev().filter_map(|id| self.by_id.get(id))
}
pub fn window(&self, id: WindowId) -> Option<&W> {
self.by_id.get(&id)
}
pub fn id_for_surface(&self, surface: &WlSurface) -> Option<WindowId> {
self.surface_ids.get(surface).copied()
}
pub fn ordered_ids(&self) -> impl DoubleEndedIterator<Item = WindowId> + '_ {
self.overview_order.iter().copied()
}
pub fn record(&self, id: WindowId) -> Option<&WindowRecord> { pub fn record(&self, id: WindowId) -> Option<&WindowRecord> {
self.records.get(&id) self.records.get(&id)
} }
@@ -74,14 +113,14 @@ impl<W: Eq + Hash> WindowRegistry<W> {
self.records.get_mut(&id) self.records.get_mut(&id)
} }
pub fn records_mut(&mut self) -> impl Iterator<Item = (&WindowId, &mut WindowRecord)> {
self.records.iter_mut()
}
pub fn records(&self) -> impl Iterator<Item = (&WindowId, &WindowRecord)> { pub fn records(&self) -> impl Iterator<Item = (&WindowId, &WindowRecord)> {
self.records.iter() self.records.iter()
} }
pub fn records_mut(&mut self) -> impl Iterator<Item = (&WindowId, &mut WindowRecord)> {
self.records.iter_mut()
}
pub fn take_world_positions(&mut self) -> HashMap<WindowId, (WorkspaceId, AnimatedValue)> { pub fn take_world_positions(&mut self) -> HashMap<WindowId, (WorkspaceId, AnimatedValue)> {
self.records self.records
.iter_mut() .iter_mut()
@@ -140,7 +179,17 @@ impl<W: Eq + Hash> WindowRegistry<W> {
pub fn remove(&mut self, window: &W) -> Option<WindowId> { pub fn remove(&mut self, window: &W) -> Option<WindowId> {
let id = self.ids.remove(window)?; let id = self.ids.remove(window)?;
self.records.remove(&id); self.by_id.remove(&id);
if let Some(record) = self.records.remove(&id)
&& let Some(surface) = record.surface
{
self.surface_ids.remove(&surface);
}
let index = self
.overview_order
.binary_search_by_key(&id.0, |id| id.0)
.expect("registered windows have an overview entry");
self.overview_order.remove(index);
Some(id) Some(id)
} }
@@ -153,10 +202,96 @@ impl<W: Eq + Hash> WindowRegistry<W> {
} }
} }
impl WindowRegistry<Window> {
pub fn register_window(&mut self, window: Window, id: WindowId) {
let surface = window.toplevel().map(|toplevel| toplevel.wl_surface().clone());
self.register(window, id);
if let Some(surface) = surface {
self.surface_ids.insert(surface.clone(), id);
self.records.get_mut(&id).unwrap().surface = Some(surface.clone());
self.refresh_metadata(&surface);
}
}
pub fn refresh_metadata(&mut self, surface: &WlSurface) -> bool {
let Some(id) = self.id_for_surface(surface) else {
return false;
};
let record = self.records.get_mut(&id).unwrap();
with_states(surface, |states| {
let attributes = states.data_map.get::<XdgToplevelSurfaceData>().unwrap().lock().unwrap();
let app_id = attributes.app_id.as_deref().unwrap_or_default();
let title = attributes.title.as_deref().unwrap_or_default();
let changed = record.app_id != app_id || record.title != title;
if changed {
record.app_id.clear();
record.app_id.push_str(app_id);
record.title.clear();
record.title.push_str(title);
record.metadata_revision = record.metadata_revision.wrapping_add(1);
}
changed
})
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn overview_order_tracks_registration_removal_and_remapping() {
let mut windows = WindowRegistry::default();
let first = windows.allocate_id();
let second = windows.allocate_id();
let third = windows.allocate_id();
// Registration need not happen in allocation order.
windows.register("second", second);
windows.register("first", first);
windows.register("third", third);
assert_eq!(
windows.overview_windows().copied().collect::<Vec<_>>(),
["third", "second", "first"]
);
windows.remove(&"second");
assert_eq!(
windows.overview_windows().copied().collect::<Vec<_>>(),
["third", "first"]
);
windows.remove(&"first");
let remapped = windows.allocate_id();
windows.register("first", remapped);
assert_eq!(
windows.overview_windows().copied().collect::<Vec<_>>(),
["first", "third"]
);
windows.remove(&"first");
windows.remove(&"third");
assert_eq!(windows.overview_windows().count(), 0);
}
#[test]
fn overview_iteration_borrows_handles_and_removal_releases_cached_handles() {
let handle = std::rc::Rc::new("surface");
let mut windows = WindowRegistry::default();
let id = windows.allocate_id();
windows.register(handle.clone(), id);
assert_eq!(std::rc::Rc::strong_count(&handle), 3);
for _ in 0..100 {
assert!(std::rc::Rc::ptr_eq(windows.overview_windows().next().unwrap(), &handle));
assert_eq!(std::rc::Rc::strong_count(&handle), 3);
}
windows.remove(&handle);
assert_eq!(std::rc::Rc::strong_count(&handle), 1);
}
#[test] #[test]
fn unmap_drops_metadata_and_late_updates_cannot_resurrect_it() { fn unmap_drops_metadata_and_late_updates_cannot_resurrect_it() {
let mut windows = WindowRegistry::default(); let mut windows = WindowRegistry::default();
+5 -10
View File
@@ -115,7 +115,7 @@ impl Ferese {
return; return;
} }
self.windows.register(window.clone(), id); self.windows.register_window(window.clone(), id);
self.windows.update(id, |w| w.placement_anchor = Some(anchor)); self.windows.update(id, |w| w.placement_anchor = Some(anchor));
if rule.width.is_none() && rule.height.is_none() && client_size(&window).is_none() && remembered_work.is_none() if rule.width.is_none() && rule.height.is_none() && client_size(&window).is_none() && remembered_work.is_none()
@@ -156,7 +156,7 @@ impl Ferese {
return; return;
} }
self.windows.register(window.clone(), id); self.windows.register_window(window.clone(), id);
self.window_stack.insert(id); self.window_stack.insert(id);
if focus_new_window { if focus_new_window {
self.focused_window = Some(id); self.focused_window = Some(id);
@@ -196,7 +196,7 @@ impl Ferese {
return; return;
} }
self.windows.register(window.clone(), id); self.windows.register_window(window.clone(), id);
self.window_stack.insert(id); self.window_stack.insert(id);
if client_size(&window).is_none() { if client_size(&window).is_none() {
self.windows.update(id, |w| w.natural_floating_pending = true); self.windows.update(id, |w| w.natural_floating_pending = true);
@@ -649,7 +649,7 @@ impl Ferese {
} }
pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool { pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool {
if !self.windows.ids().values().any(|window_id| *window_id == id) { if !self.windows.window(id).is_some() {
return false; return false;
} }
@@ -666,12 +666,7 @@ impl Ferese {
} }
pub(super) fn send_window_close(&self, id: WindowId) { pub(super) fn send_window_close(&self, id: WindowId) {
let Some(toplevel) = self let Some(toplevel) = self.windows.window(id).and_then(|window| window.toplevel()) else {
.windows
.ids()
.iter()
.find_map(|(window, window_id)| (*window_id == id).then(|| window.toplevel()).flatten())
else {
return; return;
}; };
+4
View File
@@ -234,6 +234,10 @@ impl ClientGeometry {
matches_target matches_target
} }
pub fn waiting_for_commit(&self) -> bool {
self.waiting_since.is_some()
}
pub fn timed_out(&self, now: Duration) -> bool { pub fn timed_out(&self, now: Duration) -> bool {
self.waiting_since self.waiting_since
.is_some_and(|started| now.saturating_sub(started) >= CLIENT_COMMIT_TIMEOUT) .is_some_and(|started| now.saturating_sub(started) >= CLIENT_COMMIT_TIMEOUT)
+49 -10
View File
@@ -1,3 +1,5 @@
use std::collections::HashSet;
use ferese_layout::WindowId; use ferese_layout::WindowId;
/// Most recent focus first. Layer surfaces and temporary selections do not enter this list. /// Most recent focus first. Layer surfaces and temporary selections do not enter this list.
@@ -19,19 +21,19 @@ impl FocusHistory {
self.windows.retain(|id| *id != window); self.windows.retain(|id| *id != window);
} }
pub fn candidates(&self, current: Option<WindowId>, mut available: Vec<WindowId>) -> Vec<WindowId> { pub fn candidates(&self, current: Option<WindowId>, available: impl AsRef<[WindowId]>) -> Vec<WindowId> {
available.sort_unstable_by_key(|id| id.0); let mut remaining = available.as_ref().iter().copied().collect::<HashSet<_>>();
available.dedup(); let mut ordered = Vec::with_capacity(remaining.len());
let mut ordered = Vec::with_capacity(available.len());
for id in current for id in current.into_iter().chain(self.windows.iter().copied()) {
.into_iter() if remaining.remove(&id) {
.chain(self.windows.iter().copied())
.chain(available.iter().copied())
{
if available.contains(&id) && !ordered.contains(&id) {
ordered.push(id); ordered.push(id);
} }
} }
let mut tail = remaining.into_iter().collect::<Vec<_>>();
tail.sort_unstable_by_key(|id| id.0);
ordered.extend(tail);
ordered ordered
} }
} }
@@ -62,6 +64,8 @@ impl FocusCycle {
} }
pub fn advance(&mut self, reverse: bool, available: &[WindowId]) -> Option<WindowId> { pub fn advance(&mut self, reverse: bool, available: &[WindowId]) -> Option<WindowId> {
let available = available.iter().copied().collect::<HashSet<_>>();
// Keep the old index when the selected window disappears so forward // Keep the old index when the selected window disappears so forward
// traversal selects its successor rather than skipping over it. // traversal selects its successor rather than skipping over it.
let old = self let old = self
@@ -91,6 +95,41 @@ impl FocusCycle {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn indexed_candidates_preserve_linear_order_with_duplicates_and_missing_windows() {
let mut history = FocusHistory::default();
for id in [7, 2, 5, 9, 2] {
history.record(WindowId(id));
}
for mask in 0..1024 {
let available = (0..10)
.filter(|id| mask & (1 << id) != 0)
.flat_map(|id| [WindowId(id), WindowId(id)])
.collect::<Vec<_>>();
for current in [None, Some(WindowId(2)), Some(WindowId(99))] {
let mut sorted = available.clone();
sorted.sort_unstable_by_key(|id| id.0);
sorted.dedup();
let mut expected = Vec::new();
for id in current
.into_iter()
.chain(history.windows.iter().copied())
.chain(sorted.iter().copied())
{
if sorted.contains(&id) && !expected.contains(&id) {
expected.push(id);
}
}
assert_eq!(history.candidates(current, &available), expected);
}
}
}
#[test] #[test]
fn history_is_unique_filters_unavailable_and_last_focus_toggles() { fn history_is_unique_filters_unavailable_and_last_focus_toggles() {
let mut history = FocusHistory::default(); let mut history = FocusHistory::default();
+10 -3
View File
@@ -1,4 +1,4 @@
use std::collections::{HashMap, HashSet}; use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
@@ -60,7 +60,7 @@ struct OutputState {
geometry: OutputGeometry, geometry: OutputGeometry,
active: WorkspaceId, active: WorkspaceId,
previous: Option<WorkspaceId>, previous: Option<WorkspaceId>,
workspaces: HashSet<WorkspaceId>, workspaces: BTreeSet<WorkspaceId>,
} }
impl OutputState { impl OutputState {
@@ -87,7 +87,7 @@ struct EvacuationRecord {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct OutputWorkspaceMap { pub struct OutputWorkspaceMap {
outputs: HashMap<OutputId, OutputState>, outputs: BTreeMap<OutputId, OutputState>,
assignments: HashMap<WorkspaceId, OutputId>, assignments: HashMap<WorkspaceId, OutputId>,
revisions: HashMap<WorkspaceId, u64>, revisions: HashMap<WorkspaceId, u64>,
evacuations: HashMap<OutputId, EvacuationRecord>, evacuations: HashMap<OutputId, EvacuationRecord>,
@@ -95,6 +95,13 @@ pub struct OutputWorkspaceMap {
} }
impl OutputWorkspaceMap { impl OutputWorkspaceMap {
pub fn assigned_workspaces(&self, output: OutputId) -> impl DoubleEndedIterator<Item = WorkspaceId> + '_ {
self.outputs
.get(&output)
.into_iter()
.flat_map(|state| state.workspaces.iter().copied())
}
pub fn focused_output(&self) -> Option<OutputId> { pub fn focused_output(&self) -> Option<OutputId> {
self.focused self.focused
} }
+12 -6
View File
@@ -7,7 +7,7 @@ use ferese_layout::{
SizeConstraints, ViewportFocusStrategy, WindowId, SizeConstraints, ViewportFocusStrategy, WindowId,
}; };
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct WorkspaceId(pub u64); pub struct WorkspaceId(pub u64);
#[derive(Debug)] #[derive(Debug)]
@@ -78,11 +78,17 @@ impl WorkspaceLayout {
} }
} }
pub fn window_ids(&self) -> Box<dyn Iterator<Item = WindowId> + '_> { pub fn window_ids(&self) -> impl Iterator<Item = WindowId> + '_ {
match self { let scrolling = match self {
Self::Scrolling(layout) => Box::new(layout.window_ids()), Self::Scrolling(layout) => Some(layout.window_ids()),
Self::Tree(layout) => Box::new(layout.window_ids()), Self::Tree(_) => None,
} };
let tree = match self {
Self::Tree(layout) => Some(layout.window_ids()),
Self::Scrolling(_) => None,
};
scrolling.into_iter().flatten().chain(tree.into_iter().flatten())
} }
pub fn insert( pub fn insert(
+37 -24
View File
@@ -15,17 +15,27 @@ pub struct WorkspaceView {
impl OutputWorkspaceMap { impl OutputWorkspaceMap {
pub fn workspace_views(&self, workspaces: &WorkspaceSet) -> Vec<WorkspaceView> { pub fn workspace_views(&self, workspaces: &WorkspaceSet) -> Vec<WorkspaceView> {
let mut outputs = self.connected_outputs().collect::<Vec<_>>(); self.workspace_views_iter(workspaces).collect()
outputs.sort_unstable(); }
outputs pub fn workspace_views_iter<'a>(
.into_iter() &'a self,
.flat_map(|output| { workspaces: &'a WorkspaceSet,
self.ordered_workspaces(workspaces, output) ) -> impl Iterator<Item = WorkspaceView> + 'a {
.into_iter() self.connected_outputs()
.flat_map(move |output| self.workspace_views_for_output(workspaces, output))
}
pub fn workspace_views_for_output<'a>(
&'a self,
workspaces: &'a WorkspaceSet,
output: OutputId,
) -> impl Iterator<Item = WorkspaceView> + 'a {
self.ordered_workspace_ids(workspaces, output)
.enumerate() .enumerate()
.map(move |(index, id)| { .map(move |(index, id)| {
let visible = self.active_workspace(output) == Some(id); let visible = self.active_workspace(output) == Some(id);
WorkspaceView { WorkspaceView {
id, id,
output, output,
@@ -35,26 +45,28 @@ impl OutputWorkspaceMap {
focused: visible && self.focused_output() == Some(output), focused: visible && self.focused_output() == Some(output),
} }
}) })
}) }
.collect()
pub fn ordered_workspace_ids<'a>(
&'a self,
workspaces: &'a WorkspaceSet,
output: OutputId,
) -> impl DoubleEndedIterator<Item = WorkspaceId> + 'a {
self.assigned_workspaces(output)
.filter(|id| workspaces.workspace(*id).is_some())
} }
pub fn ordered_workspaces(&self, workspaces: &WorkspaceSet, output: OutputId) -> Vec<WorkspaceId> { pub fn ordered_workspaces(&self, workspaces: &WorkspaceSet, output: OutputId) -> Vec<WorkspaceId> {
let mut ids = workspaces self.ordered_workspace_ids(workspaces, output).collect()
.iter()
.filter(|workspace| self.output_for_workspace(workspace.id) == Some(output))
.map(|workspace| workspace.id)
.collect::<Vec<_>>();
ids.sort_unstable_by_key(|id| id.0);
ids
} }
/// Out-of-range numeric selection resolves to the trailing spare. Looking /// Out-of-range numeric selection resolves to the trailing spare. Looking
/// up a gesture target never creates workspaces or changes history. /// up a gesture target never creates workspaces or changes history.
pub fn workspace_at(&self, workspaces: &WorkspaceSet, output: OutputId, index: u32) -> Option<WorkspaceId> { pub fn workspace_at(&self, workspaces: &WorkspaceSet, output: OutputId, index: u32) -> Option<WorkspaceId> {
let index = index.checked_sub(1)? as usize; let index = index.checked_sub(1)? as usize;
let ids = self.ordered_workspaces(workspaces, output); self.ordered_workspace_ids(workspaces, output)
ids.get(index).or_else(|| ids.last()).copied() .nth(index)
.or_else(|| self.ordered_workspace_ids(workspaces, output).next_back())
} }
/// Reconcile after membership, selection, output, or transition changes. /// Reconcile after membership, selection, output, or transition changes.
@@ -68,17 +80,15 @@ impl OutputWorkspaceMap {
workspaces.activate(active).expect("output's active workspace exists"); workspaces.activate(active).expect("output's active workspace exists");
} }
let mut protected = transitions.clone(); let mut protected = HashSet::new();
let outputs = self.connected_outputs().collect::<Vec<_>>(); let outputs = self.connected_outputs().collect::<Vec<_>>();
for output in outputs { for output in outputs {
let ordered = self.ordered_workspaces(workspaces, output); let last = self.ordered_workspace_ids(workspaces, output).next_back();
if let Some(active) = self.active_workspace(output) { if let Some(active) = self.active_workspace(output) {
protected.insert(active); protected.insert(active);
} }
let spare = ordered let spare = last
.last()
.copied()
.filter(|id| workspaces.workspace(*id).is_some_and(|w| w.is_empty())) .filter(|id| workspaces.workspace(*id).is_some_and(|w| w.is_empty()))
.unwrap_or_else(|| { .unwrap_or_else(|| {
let id = workspaces.create_workspace(); let id = workspaces.create_workspace();
@@ -91,7 +101,10 @@ impl OutputWorkspaceMap {
let removed = workspaces let removed = workspaces
.iter() .iter()
.filter(|workspace| { .filter(|workspace| {
workspace.is_empty() && !protected.contains(&workspace.id) && workspace.id != workspaces.active_id() workspace.is_empty()
&& !protected.contains(&workspace.id)
&& !transitions.contains(&workspace.id)
&& workspace.id != workspaces.active_id()
}) })
.map(|workspace| workspace.id) .map(|workspace| workspace.id)
.collect::<Vec<_>>(); .collect::<Vec<_>>();