micro optimization fixes
This commit is contained in:
1 parent
0d7203f84b
commit
da60ecb719
36 files changed
+1707
-447
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -30,26 +30,39 @@ 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| {
|
||||
.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();
|
||||
|
||||
if let (Some(parent), Some(child)) = (parent, child) {
|
||||
@@ -61,14 +74,15 @@ impl XdgShellHandler for Ferese {
|
||||
// Hidden workspace windows are managed but unmapped from Space.
|
||||
let Some(window) = self
|
||||
.windows
|
||||
.ids()
|
||||
.keys()
|
||||
.chain(self.space.elements())
|
||||
.find(|window| {
|
||||
.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 {
|
||||
return;
|
||||
@@ -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);
|
||||
|
||||
+33
-18
@@ -364,18 +364,14 @@ impl Ferese {
|
||||
.into_iter()
|
||||
.map(|symbol| symbol.raw())
|
||||
.collect::<Vec<_>>();
|
||||
let action = data.bindings.iter().find_map(|binding| {
|
||||
binding
|
||||
.matches(
|
||||
let action = data.bindings.action_for_key(
|
||||
keycode,
|
||||
&raw_symbols,
|
||||
modifiers.logo,
|
||||
modifiers.ctrl,
|
||||
modifiers.alt,
|
||||
modifiers.shift,
|
||||
)
|
||||
.then(|| binding.action.clone())
|
||||
});
|
||||
);
|
||||
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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use smithay::desktop::Window;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn animated_window_elements(
|
||||
@@ -109,17 +111,7 @@ pub(crate) fn sampled_output_elements(
|
||||
frame,
|
||||
));
|
||||
}
|
||||
let mut candidates = if state.overview.is_active() {
|
||||
state.windows.ids().keys().cloned().collect::<Vec<_>>()
|
||||
} else {
|
||||
state.space.elements().rev().cloned().collect::<Vec<_>>()
|
||||
};
|
||||
if state.overview.is_active() {
|
||||
candidates.sort_by_key(|window| std::cmp::Reverse(state.windows.ids().get(window).map_or(0, |id| id.0)));
|
||||
}
|
||||
let windows = candidates
|
||||
.iter()
|
||||
.filter_map(|window| {
|
||||
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) {
|
||||
@@ -137,11 +129,24 @@ pub(crate) fn sampled_output_elements(
|
||||
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<_>>();
|
||||
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()
|
||||
.filter_map(prepare_window)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
for (window, id, visual, decoration_progress, close_alpha) in windows {
|
||||
for (window, id, sample, visual, decoration_progress, close_alpha) in windows {
|
||||
let constrain = rounded_visual_rect(visual, output_geometry.loc);
|
||||
let 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,
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
//! Static uniform names stay borrowed; cached frame elements share their state.
|
||||
//! Copy on write preserves the ID and commit seen by an already sampled frame.
|
||||
use std::sync::Arc;
|
||||
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, Uniform};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions};
|
||||
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct SharedPixelShaderElement {
|
||||
shader: GlesPixelProgram,
|
||||
state: Arc<ShaderState>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ShaderState {
|
||||
id: Id,
|
||||
commit: CommitCounter,
|
||||
area: Rectangle<i32, Logical>,
|
||||
opaque: Vec<Rectangle<i32, Logical>>,
|
||||
alpha: f32,
|
||||
uniforms: Vec<Uniform<'static>>,
|
||||
kind: Kind,
|
||||
}
|
||||
|
||||
impl SharedPixelShaderElement {
|
||||
pub(crate) fn new(
|
||||
shader: GlesPixelProgram,
|
||||
area: Rectangle<i32, Logical>,
|
||||
opaque: Option<Vec<Rectangle<i32, Logical>>>,
|
||||
alpha: f32,
|
||||
uniforms: Vec<Uniform<'static>>,
|
||||
kind: Kind,
|
||||
) -> Self {
|
||||
Self {
|
||||
shader,
|
||||
state: Arc::new(ShaderState {
|
||||
id: Id::new(),
|
||||
commit: CommitCounter::default(),
|
||||
area,
|
||||
opaque: opaque.unwrap_or_default(),
|
||||
alpha,
|
||||
uniforms,
|
||||
kind,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resize(&mut self, area: Rectangle<i32, Logical>, opaque: Option<Vec<Rectangle<i32, Logical>>>) {
|
||||
let opaque = opaque.unwrap_or_default();
|
||||
|
||||
if self.state.area != area || self.state.opaque != opaque {
|
||||
let state = Arc::make_mut(&mut self.state);
|
||||
state.area = area;
|
||||
state.opaque = opaque;
|
||||
state.commit.increment();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_uniforms(&mut self, uniforms: Vec<Uniform<'static>>) {
|
||||
let state = Arc::make_mut(&mut self.state);
|
||||
state.uniforms = uniforms;
|
||||
state.commit.increment();
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for SharedPixelShaderElement {
|
||||
fn id(&self) -> &Id {
|
||||
&self.state.id
|
||||
}
|
||||
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
self.state.commit
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
Rectangle::from_size(self.state.area.size.to_f64().to_buffer(1.0, Transform::Normal))
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
self.state.area.to_physical_precise_round(scale)
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
|
||||
self.state
|
||||
.opaque
|
||||
.iter()
|
||||
.map(|region| region.to_physical_precise_round(scale))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn alpha(&self) -> f32 {
|
||||
self.state.alpha
|
||||
}
|
||||
|
||||
fn kind(&self) -> Kind {
|
||||
self.state.kind
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderElement<GlesRenderer> for SharedPixelShaderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
_opaque: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
frame.render_pixel_shader_to(
|
||||
&self.shader,
|
||||
src,
|
||||
dst,
|
||||
self.state.area.size.to_buffer(1, Transform::Normal),
|
||||
Some(damage),
|
||||
self.state.alpha,
|
||||
&self.state.uniforms,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||
use smithay::backend::renderer::gles::element::PixelShaderElement;
|
||||
use smithay::backend::renderer::gles::{GlesTexture, UniformName, UniformType};
|
||||
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn pixels(renderer: &mut GlesRenderer, element: &impl RenderElement<GlesRenderer>) -> Vec<u8> {
|
||||
let size = (32, 32).into();
|
||||
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap();
|
||||
let rect = Rectangle::from_size((32, 32).into());
|
||||
let mut target = renderer.bind(&mut texture).unwrap();
|
||||
|
||||
{
|
||||
let mut frame = renderer
|
||||
.render(&mut target, (32, 32).into(), Transform::Normal)
|
||||
.unwrap();
|
||||
frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap();
|
||||
element.draw(&mut frame, element.src(), rect, &[rect], &[]).unwrap();
|
||||
let _ = frame.finish().unwrap();
|
||||
}
|
||||
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
|
||||
.unwrap();
|
||||
renderer.map_texture(&mapping).unwrap().to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires an EGL rendering device"]
|
||||
fn shared_shader_matches_smithay_and_keeps_old_frames_immutable() {
|
||||
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
|
||||
let program = renderer
|
||||
.compile_custom_pixel_shader(
|
||||
super::super::corner_shader(super::super::ROUNDED_SOLID_SHADER),
|
||||
&[
|
||||
UniformName::new("clip_rect", UniformType::_4f),
|
||||
UniformName::new("radius", UniformType::_1f),
|
||||
UniformName::new("color", UniformType::_4f),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let area = Rectangle::new((2, 3).into(), (32, 32).into());
|
||||
let uniforms = |color| {
|
||||
vec![
|
||||
Uniform::new("clip_rect", [0.0_f32, 0.0, 32.0, 32.0]),
|
||||
Uniform::new("radius", 8.0_f32),
|
||||
Uniform::new("color", color),
|
||||
]
|
||||
};
|
||||
let red = [0.5_f32, 0.0, 0.0, 0.5];
|
||||
let green = [0.0_f32, 0.5, 0.0, 0.5];
|
||||
let opaque = Some(vec![Rectangle::from_size((20, 20).into())]);
|
||||
let baseline = PixelShaderElement::new(
|
||||
program.clone(),
|
||||
area,
|
||||
opaque.clone(),
|
||||
0.5,
|
||||
uniforms(red),
|
||||
Kind::Unspecified,
|
||||
);
|
||||
let mut shared = SharedPixelShaderElement::new(program, area, opaque, 0.5, uniforms(red), Kind::Unspecified);
|
||||
let old = shared.clone();
|
||||
let old_commit = old.current_commit();
|
||||
|
||||
assert!(Arc::ptr_eq(&shared.state, &old.state));
|
||||
assert!(
|
||||
shared
|
||||
.state
|
||||
.uniforms
|
||||
.iter()
|
||||
.all(|uniform| matches!(uniform.name, Cow::Borrowed(_)))
|
||||
);
|
||||
|
||||
for scale in [1.0, 1.25, 2.0] {
|
||||
assert_eq!(shared.geometry(scale.into()), baseline.geometry(scale.into()));
|
||||
assert_eq!(
|
||||
&*shared.opaque_regions(scale.into()),
|
||||
&*baseline.opaque_regions(scale.into())
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(pixels(&mut renderer, &shared), pixels(&mut renderer, &baseline));
|
||||
shared.resize(area, Some(vec![Rectangle::from_size((20, 20).into())]));
|
||||
assert!(Arc::ptr_eq(&shared.state, &old.state));
|
||||
shared.update_uniforms(uniforms(green));
|
||||
assert!(!Arc::ptr_eq(&shared.state, &old.state));
|
||||
assert_eq!(shared.id(), old.id());
|
||||
assert_eq!(old.current_commit(), old_commit);
|
||||
assert_ne!(shared.current_commit(), old_commit);
|
||||
assert_eq!(pixels(&mut renderer, &old), pixels(&mut renderer, &baseline));
|
||||
assert_ne!(pixels(&mut renderer, &shared), pixels(&mut renderer, &baseline));
|
||||
|
||||
let retained = shared.clone();
|
||||
shared.resize(Rectangle::from_size((16, 16).into()), None);
|
||||
assert_eq!(retained.geometry(1.0.into()), area.to_physical_precise_round(1.0));
|
||||
assert_eq!(shared.geometry(1.0.into()).size, (16, 16).into());
|
||||
assert!(shared.opaque_regions(1.0.into()).is_empty());
|
||||
assert_eq!(shared.alpha(), baseline.alpha());
|
||||
assert_eq!(shared.kind(), baseline.kind());
|
||||
}
|
||||
}
|
||||
+152
-41
@@ -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 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(),
|
||||
)
|
||||
});
|
||||
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) {
|
||||
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))
|
||||
.workspace(workspace)
|
||||
.is_some_and(|workspace| workspace.fullscreen == Some(id))
|
||||
{
|
||||
state |= ferese_shell_v1::WindowState::Fullscreen;
|
||||
}
|
||||
if self.is_floating_window(window) {
|
||||
|
||||
if matches!(
|
||||
self.workspaces.placement(id),
|
||||
Some(ferese_core::WindowPlacement::Floating { .. })
|
||||
) {
|
||||
state |= ferese_shell_v1::WindowState::Floating;
|
||||
}
|
||||
|
||||
Some(ManagedWindowSnapshot {
|
||||
id: *id,
|
||||
Some((id, workspace.0, &record.app_id, &record.title, state))
|
||||
}
|
||||
|
||||
fn managed_window_snapshots(&self) -> Vec<ManagedWindowSnapshot> {
|
||||
self.windows
|
||||
.ordered_ids()
|
||||
.filter_map(|id| self.shell_window_data(id))
|
||||
.map(|(id, workspace, app_id, title, state)| ManagedWindowSnapshot {
|
||||
id,
|
||||
workspace,
|
||||
app_id,
|
||||
title,
|
||||
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));
|
||||
|
||||
@@ -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
@@ -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 [
|
||||
|
||||
@@ -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))
|
||||
.outputs_by_id
|
||||
.get(output_id)
|
||||
.and_then(|output| self.space.output_geometry(output))
|
||||
.map(|geometry| geometry.size)
|
||||
else {
|
||||
return (0.0, 0.0);
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,16 +373,29 @@ 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 revision = self.window_stack.revision();
|
||||
let rebuild = self.stacking_cache.needs_rebuild(revision, self.space.elements().len());
|
||||
|
||||
if !rebuild && !self.stacking_cache.needs_restore() {
|
||||
return;
|
||||
}
|
||||
|
||||
if rebuild {
|
||||
let mut windows = self.space.elements().cloned().collect::<Vec<_>>();
|
||||
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 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()),
|
||||
@@ -398,13 +412,27 @@ impl Ferese {
|
||||
);
|
||||
(priority, id.map_or(usize::MAX, |id| self.window_stack.rank(id)))
|
||||
});
|
||||
if stacking_order_settled(self.space.elements(), &windows, |window| window.z_index()) {
|
||||
self.stacking_cache.replace(windows, revision);
|
||||
}
|
||||
|
||||
if !stacking_order_settled(self.space.elements(), self.stacking_cache.order(), |window| {
|
||||
window.z_index()
|
||||
}) {
|
||||
for window in self.stacking_cache.order() {
|
||||
self.space.raise_element(window, false);
|
||||
}
|
||||
}
|
||||
|
||||
self.stacking_cache.restored();
|
||||
}
|
||||
|
||||
pub(crate) fn map_window_geometry(&mut self, window: Window, location: Point<i32, Logical>) {
|
||||
if self.space.element_location(&window) == Some(location) {
|
||||
return;
|
||||
}
|
||||
|
||||
for window in windows {
|
||||
self.space.raise_element(&window, false);
|
||||
}
|
||||
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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -234,6 +234,10 @@ impl ClientGeometry {
|
||||
matches_target
|
||||
}
|
||||
|
||||
pub fn waiting_for_commit(&self) -> bool {
|
||||
self.waiting_since.is_some()
|
||||
}
|
||||
|
||||
pub fn timed_out(&self, now: Duration) -> bool {
|
||||
self.waiting_since
|
||||
.is_some_and(|started| now.saturating_sub(started) >= CLIENT_COMMIT_TIMEOUT)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use ferese_layout::WindowId;
|
||||
|
||||
/// Most recent focus first. Layer surfaces and temporary selections do not enter this list.
|
||||
@@ -19,19 +21,19 @@ impl FocusHistory {
|
||||
self.windows.retain(|id| *id != window);
|
||||
}
|
||||
|
||||
pub fn candidates(&self, current: Option<WindowId>, mut available: Vec<WindowId>) -> Vec<WindowId> {
|
||||
available.sort_unstable_by_key(|id| id.0);
|
||||
available.dedup();
|
||||
let mut ordered = Vec::with_capacity(available.len());
|
||||
for id in current
|
||||
.into_iter()
|
||||
.chain(self.windows.iter().copied())
|
||||
.chain(available.iter().copied())
|
||||
{
|
||||
if available.contains(&id) && !ordered.contains(&id) {
|
||||
pub fn candidates(&self, current: Option<WindowId>, available: impl AsRef<[WindowId]>) -> Vec<WindowId> {
|
||||
let mut remaining = available.as_ref().iter().copied().collect::<HashSet<_>>();
|
||||
let mut ordered = Vec::with_capacity(remaining.len());
|
||||
|
||||
for id in current.into_iter().chain(self.windows.iter().copied()) {
|
||||
if remaining.remove(&id) {
|
||||
ordered.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
let mut tail = remaining.into_iter().collect::<Vec<_>>();
|
||||
tail.sort_unstable_by_key(|id| id.0);
|
||||
ordered.extend(tail);
|
||||
ordered
|
||||
}
|
||||
}
|
||||
@@ -62,6 +64,8 @@ impl FocusCycle {
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, reverse: bool, available: &[WindowId]) -> Option<WindowId> {
|
||||
let available = available.iter().copied().collect::<HashSet<_>>();
|
||||
|
||||
// Keep the old index when the selected window disappears so forward
|
||||
// traversal selects its successor rather than skipping over it.
|
||||
let old = self
|
||||
@@ -91,6 +95,41 @@ impl FocusCycle {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn indexed_candidates_preserve_linear_order_with_duplicates_and_missing_windows() {
|
||||
let mut history = FocusHistory::default();
|
||||
|
||||
for id in [7, 2, 5, 9, 2] {
|
||||
history.record(WindowId(id));
|
||||
}
|
||||
|
||||
for mask in 0..1024 {
|
||||
let available = (0..10)
|
||||
.filter(|id| mask & (1 << id) != 0)
|
||||
.flat_map(|id| [WindowId(id), WindowId(id)])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for current in [None, Some(WindowId(2)), Some(WindowId(99))] {
|
||||
let mut sorted = available.clone();
|
||||
sorted.sort_unstable_by_key(|id| id.0);
|
||||
sorted.dedup();
|
||||
let mut expected = Vec::new();
|
||||
|
||||
for id in current
|
||||
.into_iter()
|
||||
.chain(history.windows.iter().copied())
|
||||
.chain(sorted.iter().copied())
|
||||
{
|
||||
if sorted.contains(&id) && !expected.contains(&id) {
|
||||
expected.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(history.candidates(current, &available), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_is_unique_filters_unavailable_and_last_focus_toggles() {
|
||||
let mut history = FocusHistory::default();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
@@ -60,7 +60,7 @@ struct OutputState {
|
||||
geometry: OutputGeometry,
|
||||
active: WorkspaceId,
|
||||
previous: Option<WorkspaceId>,
|
||||
workspaces: HashSet<WorkspaceId>,
|
||||
workspaces: BTreeSet<WorkspaceId>,
|
||||
}
|
||||
|
||||
impl OutputState {
|
||||
@@ -87,7 +87,7 @@ struct EvacuationRecord {
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct OutputWorkspaceMap {
|
||||
outputs: HashMap<OutputId, OutputState>,
|
||||
outputs: BTreeMap<OutputId, OutputState>,
|
||||
assignments: HashMap<WorkspaceId, OutputId>,
|
||||
revisions: HashMap<WorkspaceId, u64>,
|
||||
evacuations: HashMap<OutputId, EvacuationRecord>,
|
||||
@@ -95,6 +95,13 @@ pub struct OutputWorkspaceMap {
|
||||
}
|
||||
|
||||
impl OutputWorkspaceMap {
|
||||
pub fn assigned_workspaces(&self, output: OutputId) -> impl DoubleEndedIterator<Item = WorkspaceId> + '_ {
|
||||
self.outputs
|
||||
.get(&output)
|
||||
.into_iter()
|
||||
.flat_map(|state| state.workspaces.iter().copied())
|
||||
}
|
||||
|
||||
pub fn focused_output(&self) -> Option<OutputId> {
|
||||
self.focused
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use ferese_layout::{
|
||||
SizeConstraints, ViewportFocusStrategy, WindowId,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct WorkspaceId(pub u64);
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -78,11 +78,17 @@ impl WorkspaceLayout {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_ids(&self) -> Box<dyn Iterator<Item = WindowId> + '_> {
|
||||
match self {
|
||||
Self::Scrolling(layout) => Box::new(layout.window_ids()),
|
||||
Self::Tree(layout) => Box::new(layout.window_ids()),
|
||||
}
|
||||
pub fn window_ids(&self) -> impl Iterator<Item = WindowId> + '_ {
|
||||
let scrolling = match self {
|
||||
Self::Scrolling(layout) => Some(layout.window_ids()),
|
||||
Self::Tree(_) => None,
|
||||
};
|
||||
let tree = match self {
|
||||
Self::Tree(layout) => Some(layout.window_ids()),
|
||||
Self::Scrolling(_) => None,
|
||||
};
|
||||
|
||||
scrolling.into_iter().flatten().chain(tree.into_iter().flatten())
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
|
||||
@@ -15,17 +15,27 @@ pub struct WorkspaceView {
|
||||
|
||||
impl OutputWorkspaceMap {
|
||||
pub fn workspace_views(&self, workspaces: &WorkspaceSet) -> Vec<WorkspaceView> {
|
||||
let mut outputs = self.connected_outputs().collect::<Vec<_>>();
|
||||
outputs.sort_unstable();
|
||||
self.workspace_views_iter(workspaces).collect()
|
||||
}
|
||||
|
||||
outputs
|
||||
.into_iter()
|
||||
.flat_map(|output| {
|
||||
self.ordered_workspaces(workspaces, output)
|
||||
.into_iter()
|
||||
pub fn workspace_views_iter<'a>(
|
||||
&'a self,
|
||||
workspaces: &'a WorkspaceSet,
|
||||
) -> impl Iterator<Item = WorkspaceView> + 'a {
|
||||
self.connected_outputs()
|
||||
.flat_map(move |output| self.workspace_views_for_output(workspaces, output))
|
||||
}
|
||||
|
||||
pub fn workspace_views_for_output<'a>(
|
||||
&'a self,
|
||||
workspaces: &'a WorkspaceSet,
|
||||
output: OutputId,
|
||||
) -> impl Iterator<Item = WorkspaceView> + 'a {
|
||||
self.ordered_workspace_ids(workspaces, output)
|
||||
.enumerate()
|
||||
.map(move |(index, id)| {
|
||||
let visible = self.active_workspace(output) == Some(id);
|
||||
|
||||
WorkspaceView {
|
||||
id,
|
||||
output,
|
||||
@@ -35,26 +45,28 @@ impl OutputWorkspaceMap {
|
||||
focused: visible && self.focused_output() == Some(output),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn ordered_workspace_ids<'a>(
|
||||
&'a self,
|
||||
workspaces: &'a WorkspaceSet,
|
||||
output: OutputId,
|
||||
) -> impl DoubleEndedIterator<Item = WorkspaceId> + 'a {
|
||||
self.assigned_workspaces(output)
|
||||
.filter(|id| workspaces.workspace(*id).is_some())
|
||||
}
|
||||
|
||||
pub fn ordered_workspaces(&self, workspaces: &WorkspaceSet, output: OutputId) -> Vec<WorkspaceId> {
|
||||
let mut ids = workspaces
|
||||
.iter()
|
||||
.filter(|workspace| self.output_for_workspace(workspace.id) == Some(output))
|
||||
.map(|workspace| workspace.id)
|
||||
.collect::<Vec<_>>();
|
||||
ids.sort_unstable_by_key(|id| id.0);
|
||||
ids
|
||||
self.ordered_workspace_ids(workspaces, output).collect()
|
||||
}
|
||||
|
||||
/// Out-of-range numeric selection resolves to the trailing spare. Looking
|
||||
/// up a gesture target never creates workspaces or changes history.
|
||||
pub fn workspace_at(&self, workspaces: &WorkspaceSet, output: OutputId, index: u32) -> Option<WorkspaceId> {
|
||||
let index = index.checked_sub(1)? as usize;
|
||||
let ids = self.ordered_workspaces(workspaces, output);
|
||||
ids.get(index).or_else(|| ids.last()).copied()
|
||||
self.ordered_workspace_ids(workspaces, output)
|
||||
.nth(index)
|
||||
.or_else(|| self.ordered_workspace_ids(workspaces, output).next_back())
|
||||
}
|
||||
|
||||
/// Reconcile after membership, selection, output, or transition changes.
|
||||
@@ -68,17 +80,15 @@ impl OutputWorkspaceMap {
|
||||
workspaces.activate(active).expect("output's active workspace exists");
|
||||
}
|
||||
|
||||
let mut protected = transitions.clone();
|
||||
let mut protected = HashSet::new();
|
||||
let outputs = self.connected_outputs().collect::<Vec<_>>();
|
||||
for output in outputs {
|
||||
let ordered = self.ordered_workspaces(workspaces, output);
|
||||
let last = self.ordered_workspace_ids(workspaces, output).next_back();
|
||||
if let Some(active) = self.active_workspace(output) {
|
||||
protected.insert(active);
|
||||
}
|
||||
|
||||
let spare = ordered
|
||||
.last()
|
||||
.copied()
|
||||
let spare = last
|
||||
.filter(|id| workspaces.workspace(*id).is_some_and(|w| w.is_empty()))
|
||||
.unwrap_or_else(|| {
|
||||
let id = workspaces.create_workspace();
|
||||
@@ -91,7 +101,10 @@ impl OutputWorkspaceMap {
|
||||
let removed = workspaces
|
||||
.iter()
|
||||
.filter(|workspace| {
|
||||
workspace.is_empty() && !protected.contains(&workspace.id) && workspace.id != workspaces.active_id()
|
||||
workspace.is_empty()
|
||||
&& !protected.contains(&workspace.id)
|
||||
&& !transitions.contains(&workspace.id)
|
||||
&& workspace.id != workspaces.active_id()
|
||||
})
|
||||
.map(|workspace| workspace.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Reference in new issue
Block a user