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
+1781 -521

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
}
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 {
sleep_locked_outputs(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))
.is_none_or(|output| output.power_off || output.frame_pending);
if asleep {
return;
return false;
}
}
@@ -798,18 +804,18 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr
.as_mut()
.and_then(|backend| backend.devices.remove(&node))
else {
return;
return false;
};
let Some(mut output) = device.outputs.remove(&crtc) else {
restore_device(state, node, device);
return;
return false;
};
if !device.drm.is_active() || output.frame_pending {
device.outputs.insert(crtc, output);
restore_device(state, node, device);
return;
return false;
}
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 flags = planes::frame_flags(
state.output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting()),
state.output_has_animations(&output.output),
frame.animating,
state.session_lock.active,
);
let result =
@@ -919,6 +925,8 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr
if !submitted {
schedule_frame_callbacks(state, node, crtc, plan.present_at);
}
frame.animating
}
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;
}
render_output(state, node, crtc, plan);
if state.output_has_animations(identity) {
let output_animating = render_output(state, node, crtc, plan);
if output_animating {
request_frame(state, node, crtc);
}
+7 -5
View File
@@ -11,9 +11,9 @@ use std::path::PathBuf;
use appearance::*;
pub use appearance::{BorderGradient, InactiveDimSettings, MaterialStyle, ThemeSettings};
pub(crate) use bindings::physical_keymap;
use bindings::*;
pub use bindings::{Binding, BindingAction};
pub(crate) use bindings::{BindingSet, physical_keymap};
use ferese_animation::SpringConfig;
use ferese_core::LayoutMode;
use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy};
@@ -939,18 +939,19 @@ mod tests {
assert!(bindings.iter().any(|binding| {
binding.modifiers.logo
&& 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| {
binding.modifiers.logo
&& binding.modifiers.shift
&& 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| {
binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Print)
&& 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| {
binding.modifiers.logo
@@ -970,7 +971,8 @@ mod tests {
let bindings = replaced.bindings(&input).unwrap();
assert_eq!(bindings.len(), 53);
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();
+7 -2
View File
@@ -1,5 +1,10 @@
use std::sync::Arc;
use super::*;
mod index;
pub(crate) use index::BindingSet;
impl Config {
pub fn bindings(&self, input: &InputSettings) -> Result<Vec<Binding>, ConfigError> {
let mut commands = HashMap::from([
@@ -280,7 +285,7 @@ pub(super) fn parse_action(
let argv = commands
.get(command)
.ok_or_else(|| ConfigError::Binding(format!("unknown command {command:?}")))?;
Ok(BindingAction::Spawn(argv.clone()))
Ok(BindingAction::Spawn(argv.clone().into()))
}
"close" => {
no_argument()?;
@@ -440,7 +445,7 @@ pub(super) struct BindingModifiers {
#[derive(Clone, Debug, PartialEq)]
pub enum BindingAction {
None,
Spawn(Vec<String>),
Spawn(Arc<[String]>),
Close,
Exit,
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 {
pub(crate) fn needs_update(&self, target: f64) -> bool {
self.target != target || self.current != target
}
pub(crate) fn is_animating(&self) -> bool {
self.current != self.target
}
+1
View File
@@ -84,6 +84,7 @@ impl CompositorHandler for Ferese {
} else if self.windows.ids().contains_key(&window) {
self.remove_tiled_window(&window);
self.space.map_element(window.clone(), (0, 0), false);
self.invalidate_window_stacking();
self.restore_keyboard_focus();
}
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>) {
if let Some(id) = focused.and_then(|surface| {
self.windows.ids().iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface)
.then_some(*id)
})
}) {
if let Some(id) = focused.and_then(|surface| self.windows.id_for_surface(surface)) {
self.focus_history.record(id);
}
if let Some(inhibitor) = self.active_shortcuts_inhibitor.take()
@@ -152,12 +145,7 @@ impl Ferese {
{
root = parent;
}
let window = self.windows.ids().iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == &root)
.then_some(*id)
});
let window = self.windows.id_for_surface(&root);
let output = self.space.outputs().find(|output| {
window.is_some_and(|id| self.window_belongs_to_output(id, output))
|| layer_map_for_output(output)
+40 -51
View File
@@ -30,25 +30,38 @@ impl XdgShellHandler for Ferese {
// discoverable, but do not mutate/focus the layout until its initial
// commit supplies the metadata needed to resolve placement rules.
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) {
let Some(parent) = surface.parent() else {
return;
};
let parent = self.windows.ids().iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == &parent)
.then_some(*id)
});
let parent = self.windows.id_for_surface(&parent);
let child = self
.space
.elements()
.find(|window| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
.windows
.id_for_surface(surface.wl_surface())
.and_then(|id| self.windows.window(id))
.or_else(|| {
self.space.elements().find(|window| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
})
})
.cloned();
@@ -61,13 +74,14 @@ impl XdgShellHandler for Ferese {
// Hidden workspace windows are managed but unmapped from Space.
let Some(window) = self
.windows
.ids()
.keys()
.chain(self.space.elements())
.find(|window| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
.id_for_surface(surface.wl_surface())
.and_then(|id| self.windows.window(id))
.or_else(|| {
self.space.elements().find(|window| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
})
})
.cloned()
else {
@@ -186,12 +200,7 @@ impl XdgShellHandler for Ferese {
}
fn maximize_request(&mut self, surface: ToplevelSurface) {
if let Some(id) = self.windows.ids().iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
.then_some(*id)
}) {
if let Some(id) = self.windows.id_for_surface(surface.wl_surface()) {
self.set_window_maximized(id, true);
} else {
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) {
if let Some(id) = self.windows.ids().iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
.then_some(*id)
}) {
if let Some(id) = self.windows.id_for_surface(surface.wl_surface()) {
self.set_window_maximized(id, false);
} else {
surface.with_pending_state(|state| state.states.unset(ToplevelState::Maximized));
@@ -216,12 +220,7 @@ impl XdgShellHandler for Ferese {
surface: ToplevelSurface,
_output: Option<smithay::reexports::wayland_server::protocol::wl_output::WlOutput>,
) {
if let Some(window) = self.windows.ids().iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
.then_some(*id)
}) {
if let Some(window) = self.windows.id_for_surface(surface.wl_surface()) {
self.set_window_fullscreen(window, true);
} else {
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) {
if let Some(window) = self.windows.ids().iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
.then_some(*id)
}) {
if let Some(window) = self.windows.id_for_surface(surface.wl_surface()) {
self.set_window_fullscreen(window, false);
} else {
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 {
let parent = toplevel.parent().and_then(|parent| {
state.windows.ids().iter().find_map(|(candidate, id)| {
candidate
.toplevel()
.is_some_and(|surface| surface.wl_surface() == &parent)
.then_some(*id)
})
});
let parent = toplevel
.parent()
.and_then(|parent| state.windows.id_for_surface(&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);
+39 -24
View File
@@ -364,18 +364,14 @@ impl Ferese {
.into_iter()
.map(|symbol| symbol.raw())
.collect::<Vec<_>>();
let action = data.bindings.iter().find_map(|binding| {
binding
.matches(
keycode,
&raw_symbols,
modifiers.logo,
modifiers.ctrl,
modifiers.alt,
modifiers.shift,
)
.then(|| binding.action.clone())
});
let action = data.bindings.action_for_key(
keycode,
&raw_symbols,
modifiers.logo,
modifiers.ctrl,
modifiers.alt,
modifiers.shift,
);
if data.focus_cycle.is_some()
&& state == KeyState::Pressed
&& !matches!(action, Some(BindingAction::FocusMru(_)))
@@ -405,6 +401,8 @@ impl Ferese {
if state == KeyState::Pressed {
data.intercepted_keys.insert(keycode);
let action = action.clone();
if let BindingAction::FocusMru(reverse) = action {
data.cycle_focus(reverse, modifiers.alt);
} else {
@@ -814,6 +812,8 @@ impl Ferese {
return;
}
let previous = self.focused_window;
if let Some(window) = self.window_under_visual(position) {
let focused = self.windows.ids().get(&window).copied();
if let Some(focused) = focused {
@@ -833,8 +833,21 @@ impl Ferese {
} else {
// Hover transfers keyboard focus without changing the persistent
// stack: an exposed window must not cover the floats above it.
for mapped in self.space.elements() {
mapped.set_activated(mapped == &window);
if let Some(previous) = previous
.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
@@ -844,16 +857,18 @@ impl Ferese {
.clone();
keyboard.set_focus(self, Some(surface), serial);
} 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;
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 {
self.relayout();
} else {
@@ -987,11 +1002,11 @@ impl Ferese {
fn execute_binding(&mut self, action: BindingAction) {
match action {
BindingAction::None => {}
BindingAction::Spawn(mut argv) => {
let program = argv.remove(0);
BindingAction::Spawn(argv) => {
let (program, args) = argv.split_first().expect("binding commands are validated");
match Command::new(&program)
.args(argv)
match Command::new(program)
.args(args)
.env("WAYLAND_DISPLAY", &self.socket_name)
.env_remove("WAYLAND_SOCKET")
.spawn()
+93 -34
View File
@@ -84,8 +84,17 @@ pub(crate) fn init_font_loader(
Ok(())
}
type OverviewStateLabel =
HashMap<(String, u64, [u32; 4]), (MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)>;
type LabelBuffer = (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)]
pub(crate) struct OverviewState {
@@ -97,6 +106,8 @@ pub(crate) struct OverviewState {
font_requests: Option<FontRequests>,
font: Option<FontArc>,
labels: OverviewStateLabel,
window_labels: HashMap<WindowId, WindowLabel>,
label_count: usize,
}
#[derive(Clone, Debug)]
@@ -118,6 +129,8 @@ impl Default for OverviewState {
font_requests: None,
font: None,
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 {
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 {
self.motion.is_animating(spring)
}
@@ -200,6 +224,8 @@ impl OverviewState {
}
self.font = font;
self.labels.clear();
self.window_labels.clear();
self.label_count = 0;
true
}
@@ -209,8 +235,8 @@ impl OverviewState {
scale: f64,
color: [f32; 4],
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
let key = (text.to_owned(), scale.to_bits(), color.map(f32::to_bits));
if let Some(buffer) = self.labels.get(&key) {
let key = (scale.to_bits(), color.map(f32::to_bits));
if let Some(buffer) = self.labels.get(text).and_then(|styles| styles.get(&key)) {
return Some(buffer.clone());
}
let font = self.font.as_ref()?;
@@ -249,11 +275,16 @@ impl OverviewState {
}
let buffer =
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.label_count = 0;
}
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)
}
@@ -430,32 +461,53 @@ impl Ferese {
}
}
pub(crate) fn overview_window_label(
&mut self,
window: &smithay::desktop::Window,
scale: f64,
width: f64,
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
use smithay::wayland::compositor::with_states;
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('…');
}
pub(crate) fn retain_overview_labels(&mut self) {
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(
@@ -608,9 +660,7 @@ impl Ferese {
let capacity = strip_capacity(strip);
let workspaces = self
.output_workspaces
.workspace_views(&self.workspaces)
.into_iter()
.filter(|view| view.output == output_id)
.workspace_views_for_output(&self.workspaces, output_id)
.collect::<Vec<_>>();
let selected_index = workspaces
.iter()
@@ -904,6 +954,15 @@ fn directional_distance(direction: Direction, current: (f64, f64), candidate: (f
mod tests {
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 {
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::{
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::utils::{CommitCounter, DamageSet};
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::wayland::compositor::SurfaceData;
use crate::render::SharedPixelShaderElement;
pub(crate) const HANDOFF: Duration = Duration::from_millis(80);
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.
#[derive(Clone, Debug)]
pub(crate) struct PhysicalShaderElement {
pub inner: PixelShaderElement,
pub inner: SharedPixelShaderElement,
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 buffers = &mut resources.windows.entry(id).or_default().borders;
if !buffers.contexts.contains_key(&context) {
let element = PixelShaderElement::new(
let element = SharedPixelShaderElement::new(
programs.border.clone(),
geometry,
None,
@@ -164,16 +164,16 @@ pub(super) fn window_border_element(
pub(super) fn border_uniforms(parameters: &BorderParameters) -> Vec<Uniform<'static>> {
vec![
Uniform::new("clip_rect", parameters.clip_rect).into_owned(),
Uniform::new("radius", parameters.radius).into_owned(),
Uniform::new("border_width", parameters.width).into_owned(),
Uniform::new("border_color", parameters.color).into_owned(),
Uniform::new("border_color_to", parameters.color_to).into_owned(),
Uniform::new("gradient_line", parameters.gradient_line).into_owned(),
Uniform::new("focus_color", parameters.focus_color).into_owned(),
Uniform::new("focus_color_to", parameters.focus_color_to).into_owned(),
Uniform::new("focus_gradient_line", parameters.focus_gradient_line).into_owned(),
Uniform::new("focus_mix", parameters.focus_mix).into_owned(),
Uniform::new("clip_rect", parameters.clip_rect),
Uniform::new("radius", parameters.radius),
Uniform::new("border_width", parameters.width),
Uniform::new("border_color", parameters.color),
Uniform::new("border_color_to", parameters.color_to),
Uniform::new("gradient_line", parameters.gradient_line),
Uniform::new("focus_color", parameters.focus_color),
Uniform::new("focus_color_to", parameters.focus_color_to),
Uniform::new("focus_gradient_line", parameters.focus_gradient_line),
Uniform::new("focus_mix", parameters.focus_mix),
]
}
@@ -213,12 +213,12 @@ pub(super) fn window_tint_element(
};
let uniforms = |p: &BorderParameters| {
vec![
Uniform::new("visible_rect", p.clip_rect).into_owned(),
Uniform::new("material_radius", p.radius).into_owned(),
Uniform::new("tint", p.color).into_owned(),
Uniform::new("paint_mode", 0.0_f32).into_owned(),
Uniform::new("shadow_rect", p.clip_rect).into_owned(),
Uniform::new("shadow_values", [0.0_f32; 2]).into_owned(),
Uniform::new("visible_rect", p.clip_rect),
Uniform::new("material_radius", p.radius),
Uniform::new("tint", p.color),
Uniform::new("paint_mode", 0.0_f32),
Uniform::new("shadow_rect", p.clip_rect),
Uniform::new("shadow_values", [0.0_f32; 2]),
]
};
let context = renderer.context_id().erased();
@@ -228,7 +228,7 @@ pub(super) fn window_tint_element(
&mut resources.windows.entry(id).or_default().dim
};
let cached = buffers.contexts.entry(context).or_insert_with(|| CachedBorder {
element: PixelShaderElement::new(
element: SharedPixelShaderElement::new(
program.0,
geometry,
None,
@@ -293,7 +293,7 @@ pub(super) fn window_shadow_element(
let context = renderer.context_id().erased();
let buffers = &mut resources.windows.entry(id).or_default().shadow;
if !buffers.contexts.contains_key(&context) {
let element = PixelShaderElement::new(
let element = SharedPixelShaderElement::new(
programs.shadow.clone(),
bounds,
None,
@@ -331,11 +331,11 @@ pub(super) fn window_shadow_element(
pub(super) fn shadow_uniforms(parameters: &ShadowParameters) -> Vec<Uniform<'static>> {
vec![
Uniform::new("shadow_rect", parameters.shadow_rect).into_owned(),
Uniform::new("radius", parameters.radius).into_owned(),
Uniform::new("blur", parameters.blur).into_owned(),
Uniform::new("opacity", parameters.opacity).into_owned(),
Uniform::new("shadow_color", parameters.color).into_owned(),
Uniform::new("shadow_rect", parameters.shadow_rect),
Uniform::new("radius", parameters.radius),
Uniform::new("blur", parameters.blur),
Uniform::new("opacity", parameters.opacity),
Uniform::new("shadow_color", parameters.color),
]
}
+16 -16
View File
@@ -248,7 +248,7 @@ pub(super) fn material_element(
capture.dirty.clone(),
))
} else {
MaterialElement::Fill(PixelShaderElement::new(
MaterialElement::Fill(SharedPixelShaderElement::new(
program.0.clone(),
geometry,
parameters.opaque.then(|| vec![Rectangle::from_size(geometry.size)]),
@@ -264,7 +264,7 @@ pub(super) fn material_element(
.or_insert_with(|| CachedMaterial {
element: make_element(),
parameters: parameters.clone(),
shadow: PixelShaderElement::new(
shadow: SharedPixelShaderElement::new(
program.0.clone(),
parameters.shadow_bounds,
None,
@@ -356,14 +356,14 @@ pub(super) fn blur_uniform_names() -> [UniformName<'static>; 8] {
pub(super) fn blur_uniforms(p: &MaterialParameters) -> Vec<Uniform<'static>> {
vec![
Uniform::new("visible_rect", p.visible_framebuffer).into_owned(),
Uniform::new("material_radius", p.radius).into_owned(),
Uniform::new("texture_size", [p.sample_framebuffer[2], p.sample_framebuffer[3]]).into_owned(),
Uniform::new("capture_origin", [p.sample_framebuffer[0], p.sample_framebuffer[1]]).into_owned(),
Uniform::new("blur_radius", p.blur).into_owned(),
Uniform::new("presentation_alpha", p.presentation_alpha).into_owned(),
Uniform::new("background_opacity", p.background_opacity).into_owned(),
Uniform::new("tint", p.tint).into_owned(),
Uniform::new("visible_rect", p.visible_framebuffer),
Uniform::new("material_radius", p.radius),
Uniform::new("texture_size", [p.sample_framebuffer[2], p.sample_framebuffer[3]]),
Uniform::new("capture_origin", [p.sample_framebuffer[0], p.sample_framebuffer[1]]),
Uniform::new("blur_radius", p.blur),
Uniform::new("presentation_alpha", p.presentation_alpha),
Uniform::new("background_opacity", p.background_opacity),
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>> {
vec![
Uniform::new("paint_mode", paint_mode).into_owned(),
Uniform::new("shadow_rect", parameters.shadow_rect).into_owned(),
Uniform::new("shadow_values", parameters.shadow_values).into_owned(),
Uniform::new("visible_rect", parameters.visible_framebuffer).into_owned(),
Uniform::new("material_radius", parameters.radius).into_owned(),
Uniform::new("tint", parameters.tint).into_owned(),
Uniform::new("paint_mode", paint_mode),
Uniform::new("shadow_rect", parameters.shadow_rect),
Uniform::new("shadow_values", parameters.shadow_values),
Uniform::new("visible_rect", parameters.visible_framebuffer),
Uniform::new("material_radius", parameters.radius),
Uniform::new("tint", parameters.tint),
]
}
+10 -9
View File
@@ -4,6 +4,7 @@ mod materials;
mod overview;
mod resources;
mod scene;
mod shader;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -18,6 +19,7 @@ use scene::cursor_elements;
pub(crate) use scene::{
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::renderer::element::memory::MemoryRenderBufferRenderElement;
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
@@ -31,7 +33,6 @@ use smithay::backend::renderer::element::utils::{
use smithay::backend::renderer::element::{
Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage, render_elements,
};
use smithay::backend::renderer::gles::element::PixelShaderElement;
use smithay::backend::renderer::gles::{
GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, UniformName,
UniformType,
@@ -66,7 +67,7 @@ render_elements! {
Memory=MemoryRenderElement,
Solid=SolidColorRenderElement,
LockSurface=WaylandSurfaceRenderElement<GlesRenderer>,
Border=PixelShaderElement,
Border=SharedPixelShaderElement,
Blur=BlurRenderElement,
Effect=PhysicalShaderElement,
Native=NativeTextureElement,
@@ -132,7 +133,7 @@ struct BlurRenderElement {
commit: CommitCounter,
geometry: Rectangle<i32, Logical>,
capture_rect: [i32; 4],
uniforms: Vec<Uniform<'static>>,
uniforms: Arc<[Uniform<'static>]>,
}
impl BlurRenderElement {
@@ -150,7 +151,7 @@ impl BlurRenderElement {
commit: CommitCounter::default(),
geometry: parameters.sample_geometry,
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.geometry = parameters.sample_geometry;
self.capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer);
self.uniforms = blur_uniforms(parameters);
self.uniforms = blur_uniforms(parameters).into();
self.commit.increment();
}
}
@@ -327,7 +328,7 @@ struct MaterialParameters {
#[derive(Clone, Debug)]
enum MaterialElement {
Fill(PixelShaderElement),
Fill(SharedPixelShaderElement),
Blur(BlurRenderElement),
}
@@ -335,7 +336,7 @@ enum MaterialElement {
struct CachedMaterial {
element: MaterialElement,
parameters: MaterialParameters,
shadow: PixelShaderElement,
shadow: SharedPixelShaderElement,
}
#[derive(Debug, Default)]
@@ -361,7 +362,7 @@ struct BorderParameters {
#[derive(Debug)]
struct CachedBorder {
element: PixelShaderElement,
element: SharedPixelShaderElement,
parameters: BorderParameters,
}
@@ -382,7 +383,7 @@ struct ShadowParameters {
#[derive(Debug)]
struct CachedShadow {
element: PixelShaderElement,
element: SharedPixelShaderElement,
parameters: ShadowParameters,
}
+17 -19
View File
@@ -40,12 +40,12 @@ pub(super) fn overview_chrome_element(
border_uniforms(p)
} else {
vec![
Uniform::new("visible_rect", p.clip_rect).into_owned(),
Uniform::new("material_radius", p.radius).into_owned(),
Uniform::new("tint", p.color).into_owned(),
Uniform::new("paint_mode", 0.0_f32).into_owned(),
Uniform::new("shadow_rect", p.clip_rect).into_owned(),
Uniform::new("shadow_values", [0.0_f32; 2]).into_owned(),
Uniform::new("visible_rect", p.clip_rect),
Uniform::new("material_radius", p.radius),
Uniform::new("tint", p.color),
Uniform::new("paint_mode", 0.0_f32),
Uniform::new("shadow_rect", p.clip_rect),
Uniform::new("shadow_values", [0.0_f32; 2]),
]
}
};
@@ -61,7 +61,7 @@ pub(super) fn overview_chrome_element(
.or_default()
.entry(context)
.or_insert_with(|| CachedBorder {
element: PixelShaderElement::new(
element: SharedPixelShaderElement::new(
program,
geometry,
None,
@@ -119,33 +119,31 @@ pub(super) fn overview_strip_elements(
.opacity;
let mut elements = Vec::new();
let programs = rounded_clip_program(&mut state.render, renderer);
// Resolve IDs once per strip, rather than scanning all managed windows
// separately for every miniature. Own the handles so rendering can mutate state.
let windows_by_id: std::collections::HashMap<_, _> = state
.windows
.ids()
state.retain_overview_labels();
let card_ids = cards
.iter()
.map(|(window, id)| (*id, window.clone()))
.collect();
.map(|card| card.workspace.0)
.collect::<std::collections::HashSet<_>>();
if let Some(output_id) = state.output_id(output)
&& let Some(cache) = state.render.outputs.get_mut(&output_id)
{
cache.chrome.retain(|(id, part), _| match part {
OverviewChromePart::Caption => windows_by_id.keys().any(|window| window.0 == *id),
OverviewChromePart::Card | OverviewChromePart::Outline => cards.iter().any(|card| card.workspace.0 == *id),
OverviewChromePart::Caption => state.windows.window(ferese_layout::WindowId(*id)).is_some(),
OverviewChromePart::Card | OverviewChromePart::Outline => card_ids.contains(id),
OverviewChromePart::Strip => true,
});
}
// Cached title textures are independent of window content and stay readable
// when thumbnails are small. Their pills use the shell's material and radius.
for (id, window) in &windows_by_id {
for id in frame.windows.keys() {
if !state.window_belongs_to_output(*id, output) {
continue;
}
let Some(rect) = frame.windows.get(id).map(|sample| sample.rect) else {
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;
};
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 {
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(
renderer,
window,
+40 -38
View File
@@ -1,3 +1,5 @@
use smithay::desktop::Window;
use super::*;
pub(crate) fn animated_window_elements(
@@ -109,39 +111,42 @@ pub(crate) fn sampled_output_elements(
frame,
));
}
let mut candidates = if state.overview.is_active() {
state.windows.ids().keys().cloned().collect::<Vec<_>>()
let prepare_window = |window: &Window| {
// Configure immediately, but present the frame/shadow only after
// the first buffer commit has settled the actual client geometry.
if !state.window_content_ready(window) {
return None;
}
let id = *state.windows.ids().get(window)?;
// Scrolling columns may sit outside their monitor's rectangle.
// They must not reappear on a neighboring output just because
// their global animated coordinates overlap it.
if !state.window_belongs_to_output(id, output) {
return None;
}
let sample = frame.windows.get(&id)?;
let visual = scaled_visual_rect(sample.rect, sample.close_scale);
let close_alpha = sample.close_alpha;
let decoration_progress = sample.geometry.decorations.clamp(0.0, 1.0);
Some((window.clone(), id, sample, visual, decoration_progress, close_alpha))
};
let windows = if state.overview.is_active() {
state
.windows
.overview_windows()
.filter_map(prepare_window)
.collect::<Vec<_>>()
} else {
state.space.elements().rev().cloned().collect::<Vec<_>>()
state
.space
.elements()
.rev()
.filter_map(prepare_window)
.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
// the first buffer commit has settled the actual client geometry.
if !state.window_content_ready(window) {
return None;
}
let id = *state.windows.ids().get(window)?;
// Scrolling columns may sit outside their monitor's rectangle.
// They must not reappear on a neighboring output just because
// their global animated coordinates overlap it.
if !state.window_belongs_to_output(id, output) {
return None;
}
let sample = frame.windows.get(&id)?;
let visual = scaled_visual_rect(sample.rect, sample.close_scale);
let close_alpha = sample.close_alpha;
let decoration_progress = sample.geometry.decorations.clamp(0.0, 1.0);
Some((window.clone(), id, visual, decoration_progress, close_alpha))
})
.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 material_surface = window
.toplevel()
@@ -158,10 +163,10 @@ pub(crate) fn sampled_output_elements(
let corners = RoundedRect::new(visual, output_geometry.loc, scale, window_radius);
let pixels = corners.rect;
// 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 dim = frame.windows[&id].dim;
let dim = sample.dim;
if let Some(overlay) = window_tint_element(
&mut state.render,
renderer,
@@ -182,7 +187,7 @@ pub(crate) fn sampled_output_elements(
let shadow_blur = state.theme_settings.shadow_blur;
let shadow_opacity = state.theme_settings.shadow_opacity;
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
+ (state.theme_settings.focus_ring_width - state.theme_settings.border_width) * focus;
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,
program: Some(programs.texture.clone()),
uniforms: vec![
Uniform::new("clip_rect", clip).into_owned(),
Uniform::new("radius", corners.radius).into_owned(),
],
uniforms: vec![Uniform::new("clip_rect", clip), Uniform::new("radius", corners.radius)],
}
.into(),
);
@@ -260,7 +262,7 @@ pub(crate) fn sampled_output_elements(
corners,
scale,
close_alpha,
frame.windows[&id].geometry.presentation_changed,
sample.geometry.presentation_changed,
output,
programs.clone(),
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());
}
}
+165 -54
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_manager_v1, ferese_shell_v1};
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::private_client::ClientCapabilities;
@@ -270,10 +268,15 @@ impl Ferese {
self.last_shell_snapshot = None;
return;
}
let snapshot = self.shell_snapshot();
if self.last_shell_snapshot.as_ref() == Some(&snapshot) {
if self
.last_shell_snapshot
.as_ref()
.is_some_and(|snapshot| self.shell_snapshot_matches(snapshot))
{
return;
}
let snapshot = self.shell_snapshot();
for resource in &self.shell_resources {
if let Ok(shell) = resource.upgrade() {
self.shell_snapshot_serial = self.shell_snapshot_serial.wrapping_add(1);
@@ -283,6 +286,65 @@ impl Ferese {
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 {
ShellSnapshot {
outputs: self.output_snapshots(),
@@ -319,8 +381,7 @@ impl Ferese {
fn workspace_snapshots(&self) -> Vec<WorkspaceSnapshot> {
self.output_workspaces
.workspace_views(&self.workspaces)
.into_iter()
.workspace_views_iter(&self.workspaces)
.map(|view| WorkspaceSnapshot {
id: view.id,
output: Some(view.output),
@@ -333,56 +394,46 @@ impl Ferese {
.collect()
}
fn shell_window_data(&self, id: WindowId) -> Option<(WindowId, u64, &str, &str, ferese_shell_v1::WindowState)> {
let workspace = self.workspaces.workspace_for_window(id)?;
let record = self.windows.record(id)?;
self.windows.window(id)?.toplevel()?;
let mut state = ferese_shell_v1::WindowState::empty();
if self.focused_window == Some(id) {
state |= ferese_shell_v1::WindowState::Focused;
}
if self
.workspaces
.workspace(workspace)
.is_some_and(|workspace| workspace.fullscreen == Some(id))
{
state |= ferese_shell_v1::WindowState::Fullscreen;
}
if matches!(
self.workspaces.placement(id),
Some(ferese_core::WindowPlacement::Floating { .. })
) {
state |= ferese_shell_v1::WindowState::Floating;
}
Some((id, workspace.0, &record.app_id, &record.title, state))
}
fn managed_window_snapshots(&self) -> Vec<ManagedWindowSnapshot> {
let mut snapshots = self
.windows
.ids()
.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();
if self.focused_window == Some(*id) {
state |= ferese_shell_v1::WindowState::Focused;
}
if self
.workspaces
.workspace_for_window(*id)
.and_then(|workspace| self.workspaces.workspace(workspace))
.is_some_and(|workspace| workspace.fullscreen == Some(*id))
{
state |= ferese_shell_v1::WindowState::Fullscreen;
}
if self.is_floating_window(window) {
state |= ferese_shell_v1::WindowState::Floating;
}
Some(ManagedWindowSnapshot {
id: *id,
workspace,
app_id,
title,
state,
})
self.windows
.ordered_ids()
.filter_map(|id| self.shell_window_data(id))
.map(|(id, workspace, app_id, title, state)| ManagedWindowSnapshot {
id,
workspace,
app_id: app_id.to_owned(),
title: title.to_owned(),
state,
})
.collect::<Vec<_>>();
snapshots.sort_by_key(|snapshot| snapshot.id.0);
snapshots
.collect()
}
fn handle_shell_window_request(
@@ -530,6 +581,66 @@ fn send_request_failed(shell: &FereseShellV1, request: ferese_shell_v1::FailedRe
#[cfg(test)]
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]
fn logout_serial_cannot_authorize_a_different_session_query() {
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 {
order: Vec<WindowId>,
ranks: HashMap<WindowId, usize>,
revision: u64,
}
impl WindowStack {
@@ -28,12 +29,14 @@ impl WindowStack {
if let std::collections::hash_map::Entry::Vacant(entry) = self.ranks.entry(id) {
entry.insert(self.order.len());
self.order.push(id);
self.revision = self.revision.wrapping_add(1);
}
}
pub fn remove(&mut self, id: WindowId) {
if let Some(rank) = self.ranks.remove(&id) {
self.order.remove(rank);
self.revision = self.revision.wrapping_add(1);
for (rank, candidate) in self.order.iter().enumerate().skip(rank) {
self.ranks.insert(*candidate, rank);
}
@@ -41,6 +44,10 @@ impl WindowStack {
}
pub fn raise(&mut self, id: WindowId) {
if self.order.last() == Some(&id) {
return;
}
self.remove(id);
self.insert(id);
}
@@ -48,12 +55,113 @@ impl WindowStack {
pub fn rank(&self, id: WindowId) -> usize {
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)]
mod tests {
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]
fn cached_ranks_match_reference_order_after_mixed_operations() {
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 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::dimming::DimAnimation;
use crate::gestures::{Swipe, SwipeDirection};
@@ -91,7 +91,7 @@ use crate::portal_session::PortalSession;
use crate::portal_shortcuts::PortalShortcuts;
use crate::session_lock::{IdleSettings, Lock};
use crate::shell_control::ShellSnapshot;
use crate::stacking::WindowStack;
use crate::stacking::{StackingCache, WindowStack};
use crate::wallpaper::{WallpaperConfig, WallpaperState};
use crate::window_rules::{WindowRule, resolve as resolve_window_rules};
use crate::winit::NestedBackend;
@@ -228,8 +228,13 @@ impl WorkspaceSlide {
}
fn progress(&self) -> f64 {
self.progress_at(Duration::ZERO)
}
fn progress_at(&self, delta: Duration) -> f64 {
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)
}
#[cfg(test)]
fn offset(&self, workspace: WorkspaceId, width: f64, height: f64) -> (f64, f64) {
let progress = self.progress();
let position = self
@@ -323,12 +329,15 @@ pub struct Ferese {
pub workspaces: WorkspaceSet,
pub output_workspaces: OutputWorkspaceMap,
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>,
pub(crate) windows: WindowRegistry<Window>,
pub(crate) render: crate::render::RenderResources,
pub(crate) nested_backend: Option<NestedBackend>,
pub(crate) wallpaper: WallpaperState,
window_stack: WindowStack,
stacking_cache: StackingCache<Window>,
floating_above_fullscreen: HashMap<WindowId, WindowId>,
floating_memory: crate::floating::Memory,
floating_save_worker: crate::floating::SaveWorker,
@@ -341,13 +350,14 @@ pub struct Ferese {
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
focus_swipe: Option<FocusSwipe>,
workspace_slides: HashMap<OutputId, WorkspaceSlide>,
workspace_slide_offsets: HashMap<WorkspaceId, (f64, f64)>,
pub focused_window: Option<WindowId>,
pub(crate) focus_history: ferese_core::FocusHistory,
pub(crate) focus_cycle: Option<ferese_core::FocusCycle>,
column_width_presets: Vec<ColumnWidth>,
gap_config: GapConfig,
pub(crate) input_settings: InputSettings,
pub(crate) bindings: Vec<Binding>,
pub(crate) bindings: BindingSet,
workspace_auto_back_and_forth: bool,
window_rules: Vec<WindowRule>,
pub(crate) theme_settings: ThemeSettings,
@@ -543,12 +553,15 @@ impl Ferese {
),
output_workspaces: OutputWorkspaceMap::default(),
output_ids: HashMap::new(),
outputs_by_id: HashMap::new(),
output_names: HashMap::new(),
output_identity_ids: HashMap::new(),
windows: WindowRegistry::default(),
render: Default::default(),
nested_backend: None,
wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())),
window_stack: WindowStack::default(),
stacking_cache: StackingCache::default(),
floating_above_fullscreen: HashMap::new(),
floating_memory: crate::floating::Memory::path()
.map(|p| crate::floating::Memory::load(&p))
@@ -563,13 +576,14 @@ impl Ferese {
viewport_animations: HashMap::new(),
focus_swipe: None,
workspace_slides: HashMap::new(),
workspace_slide_offsets: HashMap::new(),
focused_window: None,
focus_history: Default::default(),
focus_cycle: None,
column_width_presets: config.column_width_presets,
gap_config: config.gap_config,
input_settings: config.input_settings,
bindings: config.bindings,
bindings: config.bindings.into(),
workspace_auto_back_and_forth: config.workspace_auto_back_and_forth,
window_rules: config.window_rules,
theme_settings: config.theme_settings,
@@ -707,7 +721,7 @@ impl Ferese {
.map_err(|error| error.to_string())?;
self.gap_config = config.gap_config;
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.portal_shortcuts.reconcile(&self.bindings, &self.input_settings);
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;
if !self.animations_enabled {
self.workspace_slides.clear();
self.workspace_slide_offsets.clear();
}
self.animation_speed = config.animation_speed;
self.spring_config = config.spring_config;
@@ -976,6 +991,22 @@ impl ClientData for ClientState {
#[cfg(test)]
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]
fn configured_motion_policy_scales_durations_and_reduced_motion_wins() {
for (source, milliseconds) in [
+239 -56
View File
@@ -20,7 +20,56 @@ impl Ferese {
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 {
if !self.animations_need_tick() {
self.sync_window_stacking();
return false;
}
let delta = delta.mul_f64(self.animation_speed);
if self
.focus_swipe
@@ -30,21 +79,6 @@ impl Ferese {
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 completed_slides = Vec::new();
for (output, slide) in &mut self.workspace_slides {
@@ -102,9 +136,12 @@ impl Ferese {
dim_changed |= previous != focus.current;
}
for (_, id) in &windows {
let target = crate::dimming::target(dim_settings, self.focused_window, *id, self.overview.is_presenting());
let Some(record) = self.windows.record_mut(*id) else {
for window in self.space.elements() {
let Some(id) = self.windows.ids().get(window).copied() 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;
};
@@ -184,7 +221,11 @@ impl Ferese {
// Keep scheduling frames while waiting, so the deadline cannot stall.
active_animation |= !blocked_workspaces.is_empty();
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;
}
@@ -202,8 +243,13 @@ impl Ferese {
}
}
let mut settled_coupled_widths = Vec::new();
for (window, id) in windows {
let mut remaps = Vec::new();
for window in self.space.elements() {
let Some(id) = self.windows.ids().get(window).copied() else {
continue;
};
if self
.workspaces
.workspace_for_window(id)
@@ -229,6 +275,11 @@ impl Ferese {
geometry.visual.velocity.width = 0.0;
}
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 {
geometry.visual.target.width = target;
}
@@ -262,7 +313,7 @@ impl Ferese {
geometry.visual.current.width = width.current;
geometry.visual.velocity.width = width.velocity;
if !width_active {
settled_coupled_widths.push(id);
record.coupled_width = None;
}
}
@@ -287,12 +338,18 @@ impl Ferese {
}
let visual = geometry.visual.current;
self.space
.map_element(window, (visual.x.round() as i32, visual.y.round() as i32), false);
let location = (visual.x.round() as i32, visual.y.round() as i32).into();
if self.space.element_location(window) != Some(location) {
remaps.push((id, location));
}
}
for id in settled_coupled_widths {
self.windows.update(id, |record| record.coupled_width = None);
for (id, location) in remaps {
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() {
@@ -320,6 +377,7 @@ impl Ferese {
active_animation |= self
.overview
.advance(delta, self.spring_config, self.animations_enabled);
self.refresh_workspace_slide_offsets();
self.sync_window_stacking();
if active_animation || dim_changed {
@@ -337,48 +395,173 @@ impl Ferese {
}
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) {
let Some(workspace) = self.workspaces.workspace_for_window(window) else {
return (0.0, 0.0);
};
pub(super) fn sample_workspace_slide_offsets(
&self,
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
.workspace_slides
.iter()
.find_map(|(output, slide)| slide.contains(workspace).then_some(*output))
.or_else(|| self.output_workspaces.output_for_workspace(workspace))
else {
return (0.0, 0.0);
};
pub(super) fn refresh_workspace_slide_offsets(&mut self) {
// Reuse the input snapshot's allocation. Predicted render snapshots
// remain separate and never change hit-testing or gesture state.
let mut offsets = std::mem::take(&mut self.workspace_slide_offsets);
offsets.clear();
self.fill_workspace_slide_offsets(None, Duration::ZERO, &mut offsets);
self.workspace_slide_offsets = offsets;
}
let Some(slide) = self.workspace_slides.get(&output_id) else {
return (0.0, 0.0);
};
fn fill_workspace_slide_offsets(
&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
.output_ids
.iter()
.find(|(_, id)| **id == output_id)
.and_then(|(output, _)| self.space.output_geometry(output))
.map(|geometry| geometry.size)
else {
return (0.0, 0.0);
};
let Some(size) = self
.outputs_by_id
.get(output_id)
.and_then(|output| self.space.output_geometry(output))
.map(|geometry| geometry.size)
else {
continue;
};
let progress = slide.progress_at(delta);
let mut slide = slide.clone();
if slide.held_progress.is_none() && !delta.is_zero() {
slide.advance(delta);
for item in &slide.items {
let position = item.start.between(item.target, progress);
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 {
let active = !self.workspace_slides.is_empty();
self.workspace_slides.clear();
self.workspace_slide_offsets.clear();
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,
None => return,
};
if let Some(handle) = self
.windows
.ids()
.iter()
.find_map(|(handle, id)| (*id == window).then_some(handle.clone()))
{
if let Some(handle) = self.windows.window(window).cloned() {
if matches!(
self.workspaces.placement(window),
Some(WindowPlacement::Floating { .. })
@@ -178,6 +173,10 @@ impl Ferese {
.geometry
.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())
&& 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.coupled_width = None);
self.space.map_element(window.clone(), location, false);
self.map_window_geometry(window.clone(), location);
self.sync_window_stacking();
// Move/resize/cancel callbacks run with Smithay's pointer mutex held.
// Cursor rendering reads current_location(), which would lock it again.
@@ -269,14 +268,7 @@ impl Ferese {
let (app, parent) = Self::floating_metadata(window);
let parent_rect = parent
.as_ref()
.and_then(|surface| {
self.windows.ids().iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|top| top.wl_surface() == surface)
.then_some(*id)
})
})
.and_then(|surface| self.windows.id_for_surface(surface))
.and_then(|parent| self.presented_window_rect(parent))
.and_then(|rect| intersection(rect, work));
let dialog = crate::window_rules::is_native_dialog(app.as_deref());
@@ -420,10 +412,9 @@ impl Ferese {
.workspaces
.workspace_for_window(window)
.and_then(|workspace| self.output_workspaces.output_for_workspace(workspace));
self.output_ids
.iter()
.find(|(_, id)| Some(**id) == output_id)
.and_then(|(output, _)| self.output_bounds_for(output))
output_id
.and_then(|id| self.outputs_by_id.get(&id))
.and_then(|output| self.output_bounds_for(output))
.or_else(|| self.output_bounds())
}
+2 -2
View File
@@ -49,7 +49,7 @@ impl Ferese {
let available = self.focus_candidates();
if !preview {
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);
if let Some(id) = cycle.advance(reverse, &available) {
self.activate_managed_window(id);
@@ -57,7 +57,7 @@ impl Ferese {
return;
}
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 {
return;
}
+1 -4
View File
@@ -166,10 +166,7 @@ impl Ferese {
};
if overview_active {
let mut candidates = self.windows.ids().keys().cloned().collect::<Vec<_>>();
// 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)
self.windows.overview_windows().find_map(hit)
} else {
self.space.elements().rev().find_map(hit)
}
+54 -26
View File
@@ -59,6 +59,7 @@ impl Ferese {
// Consume idle time before setting new targets; it must not become a
// large first animation step after a keypress on an idle desktop.
self.advance_animations(Instant::now());
self.stacking_cache.invalidate();
self.arrange_layers();
let mut previous_scrolling_world_x = self.windows.take_world_positions();
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);
self.space.map_element(window.clone(), location, false);
self.map_window_geometry(window.clone(), location.into());
if let Some(toplevel) = window.toplevel() {
let state_changed = toplevel.with_pending_state(|state| {
if natural_pending {
@@ -372,39 +373,66 @@ impl Ferese {
}
self.space.raise_element(window, activate);
self.stacking_cache.remapped();
self.sync_window_stacking();
}
pub(crate) fn invalidate_window_stacking(&mut self) {
self.stacking_cache.invalidate();
}
pub(crate) fn sync_window_stacking(&mut self) {
let mut windows = self.space.elements().cloned().collect::<Vec<_>>();
windows.sort_by_key(|window| {
let id = self.windows.ids().get(window).copied();
let geometry = id.and_then(|id| self.windows.geometry(&id));
let floating =
id.is_some_and(|id| matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. })));
let priority = crate::stacking::layer_priority(
floating,
geometry.is_some_and(|geometry| geometry.is_zooming()),
geometry.is_some_and(|geometry| geometry.is_fullscreen()),
floating
&& id.is_some_and(|id| {
self.floating_above_fullscreen.get(&id).is_some_and(|parent| {
self.workspaces
.workspace_for_window(id)
.and_then(|workspace| self.workspaces.workspace(workspace))
.is_some_and(|workspace| workspace.fullscreen == Some(*parent))
})
}),
);
(priority, id.map_or(usize::MAX, |id| self.window_stack.rank(id)))
});
if stacking_order_settled(self.space.elements(), &windows, |window| window.z_index()) {
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;
}
for window in windows {
self.space.raise_element(&window, false);
if rebuild {
let mut windows = self.space.elements().cloned().collect::<Vec<_>>();
windows.sort_by_key(|window| {
let id = self.windows.ids().get(window).copied();
let geometry = id.and_then(|id| self.windows.geometry(&id));
let floating = id
.is_some_and(|id| matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. })));
let priority = crate::stacking::layer_priority(
floating,
geometry.is_some_and(|geometry| geometry.is_zooming()),
geometry.is_some_and(|geometry| geometry.is_fullscreen()),
floating
&& id.is_some_and(|id| {
self.floating_above_fullscreen.get(&id).is_some_and(|parent| {
self.workspaces
.workspace_for_window(id)
.and_then(|workspace| self.workspaces.workspace(workspace))
.is_some_and(|workspace| workspace.fullscreen == Some(*parent))
})
}),
);
(priority, id.map_or(usize::MAX, |id| self.window_stack.rank(id)))
});
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;
}
self.space.map_element(window, location, false);
self.stacking_cache.remapped();
}
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");
}
let Some(window) = self
.windows
.ids()
.iter()
.find_map(|(window, id)| (*id == next).then(|| window.clone()))
else {
let Some(window) = self.windows.window(next).cloned() else {
return;
};
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 {
let Some(window) = self
.windows
.ids()
.iter()
.find_map(|(window, window_id)| (*window_id == id).then(|| window.clone()))
else {
let Some(window) = self.windows.window(id).cloned() else {
return false;
};
if !activate_window_workspace(&mut self.workspaces, &mut self.output_workspaces, id) {
@@ -441,6 +431,7 @@ impl Ferese {
.get_mut(&output)
.expect("gesture output exists")
.held_progress = Some(progress);
self.refresh_workspace_slide_offsets();
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
self.cursor_redraw_pending = true;
return;
@@ -555,14 +546,11 @@ impl Ferese {
self.focus_lock_surface();
return;
}
let surface = self.focused_window.and_then(|focused| {
self.windows.ids().iter().find_map(|(window, id)| {
(*id == focused)
.then(|| window.toplevel())
.flatten()
.map(|toplevel| toplevel.wl_surface().clone())
})
});
let surface = self
.focused_window
.and_then(|focused| self.windows.window(focused))
.and_then(|window| window.toplevel())
.map(|toplevel| toplevel.wl_surface().clone());
self.seat.get_keyboard().expect("seat has a keyboard").set_focus(
self,
+5 -3
View File
@@ -29,6 +29,8 @@ impl Ferese {
match registration {
Ok(_) => {
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();
let visible = self.visible_workspace_ids();
self.reconcile_workspaces(&visible);
@@ -45,6 +47,8 @@ impl Ferese {
return;
};
self.outputs_by_id.remove(&output_id);
self.output_names.remove(&output_id);
self.workspace_slides.remove(&output_id);
self.render.remove_output(output_id);
self.pending_screencopies.retain(|capture| {
@@ -113,9 +117,7 @@ impl Ferese {
pub(crate) fn focused_output(&self) -> Option<&Output> {
let focused = self.output_workspaces.focused_output()?;
self.output_ids
.iter()
.find_map(|(output, id)| (*id == focused).then_some(output))
self.outputs_by_id.get(&focused)
}
pub(crate) fn restore_output_focus(&mut self) {
+153 -15
View File
@@ -5,6 +5,8 @@ use super::*;
pub(crate) struct FrameScene {
pub windows: HashMap<WindowId, WindowFrame>,
pub overview: crate::overview::OverviewMotion,
// Plane selection and scheduling reuse the activity sampled with this scene.
pub animating: bool,
}
pub(crate) struct WindowFrame {
@@ -17,6 +19,31 @@ pub(crate) struct WindowFrame {
}
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 {
if self.overview.is_animating(self.spring_config) || !self.dismissing_popups.is_empty() {
return true;
@@ -30,17 +57,17 @@ impl Ferese {
return true;
}
self.windows.records().any(|(id, record)| {
self.output_window_records(output, false).any(|(id, record, _)| {
let Some(mut geometry) = record.geometry else {
return false;
};
if !self.window_belongs_to_output(*id, output) {
if !self.window_belongs_to_output(id, output) {
return false;
}
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.focus.as_ref().is_some_and(|focus| focus.is_animating())
|| record.dimming.as_ref().is_some_and(|dim| dim.is_animating())
@@ -52,7 +79,7 @@ impl Ferese {
return true;
}
let Some(workspace) = self.workspaces.workspace_for_window(*id) else {
let Some(workspace) = self.workspaces.workspace_for_window(id) else {
return false;
};
@@ -71,28 +98,28 @@ impl Ferese {
}
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)
} else {
Duration::ZERO
};
let overview = self.overview.sample(delta, self.spring_config);
let blocked = self
.windows
.resizing()
.filter_map(|id| self.workspaces.workspace_for_window(*id))
.collect::<HashSet<_>>();
let predicted_slide_offsets =
(!delta.is_zero()).then(|| self.sample_workspace_slide_offsets(self.output_id(output), delta));
let slide_offsets = predicted_slide_offsets
.as_ref()
.unwrap_or(&self.workspace_slide_offsets);
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 };
if !self.window_belongs_to_output(id, output) {
continue;
}
let workspace = self.workspaces.workspace_for_window(id);
let world = record.world_x.as_ref().and_then(|(workspace, world)| {
let viewport = self.viewport_animations.get(workspace)?;
let held = self
@@ -103,7 +130,7 @@ impl Ferese {
Some((*world, *viewport, held))
});
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
} else {
predict_geometry(
@@ -118,7 +145,11 @@ impl Ferese {
let mut rect = overview.presented_rect(id, geometry.visual.current);
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.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);
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.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.
state.windows.set_transaction(
id,
@@ -278,6 +410,12 @@ mod tests {
state.animations_enabled = false;
let forecast = state.sample_frame(&output, Duration::from_millis(16));
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]
+141 -6
View File
@@ -4,6 +4,10 @@ use std::hash::Hash;
use ferese_animation::{AnimatedValue, WindowGeometry};
use ferese_core::WorkspaceId;
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;
@@ -12,6 +16,10 @@ use crate::resize_transaction::ResizeTransaction;
#[derive(Default)]
pub(crate) struct WindowRecord {
pub geometry: Option<WindowGeometry>,
pub surface: Option<WlSurface>,
pub app_id: String,
pub title: String,
pub metadata_revision: u64,
pub resize: Option<ResizeTransaction>,
pub focus: 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.
pub(crate) struct WindowRegistry<W> {
ids: HashMap<W, WindowId>,
by_id: HashMap<WindowId, W>,
surface_ids: HashMap<WlSurface, WindowId>,
pub(super) records: HashMap<WindowId, WindowRecord>,
// Ascending IDs; overview reverses this for front-to-back traversal.
overview_order: Vec<WindowId>,
next_id: u64,
}
@@ -40,7 +52,10 @@ impl<W> Default for WindowRegistry<W> {
fn default() -> Self {
Self {
ids: HashMap::new(),
by_id: HashMap::new(),
surface_ids: HashMap::new(),
records: HashMap::new(),
overview_order: Vec::new(),
next_id: 1,
}
}
@@ -54,10 +69,16 @@ impl<W: Eq + Hash> WindowRegistry<W> {
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.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.records.insert(id, WindowRecord::default());
}
@@ -66,6 +87,24 @@ impl<W: Eq + Hash> WindowRegistry<W> {
&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> {
self.records.get(&id)
}
@@ -74,14 +113,14 @@ impl<W: Eq + Hash> WindowRegistry<W> {
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)> {
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)> {
self.records
.iter_mut()
@@ -140,7 +179,17 @@ impl<W: Eq + Hash> WindowRegistry<W> {
pub fn remove(&mut self, window: &W) -> Option<WindowId> {
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)
}
@@ -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)]
mod tests {
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]
fn unmap_drops_metadata_and_late_updates_cannot_resurrect_it() {
let mut windows = WindowRegistry::default();
+5 -10
View File
@@ -115,7 +115,7 @@ impl Ferese {
return;
}
self.windows.register(window.clone(), id);
self.windows.register_window(window.clone(), id);
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()
@@ -156,7 +156,7 @@ impl Ferese {
return;
}
self.windows.register(window.clone(), id);
self.windows.register_window(window.clone(), id);
self.window_stack.insert(id);
if focus_new_window {
self.focused_window = Some(id);
@@ -196,7 +196,7 @@ impl Ferese {
return;
}
self.windows.register(window.clone(), id);
self.windows.register_window(window.clone(), id);
self.window_stack.insert(id);
if client_size(&window).is_none() {
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 {
if !self.windows.ids().values().any(|window_id| *window_id == id) {
if !self.windows.window(id).is_some() {
return false;
}
@@ -666,12 +666,7 @@ impl Ferese {
}
pub(super) fn send_window_close(&self, id: WindowId) {
let Some(toplevel) = self
.windows
.ids()
.iter()
.find_map(|(window, window_id)| (*window_id == id).then(|| window.toplevel()).flatten())
else {
let Some(toplevel) = self.windows.window(id).and_then(|window| window.toplevel()) else {
return;
};