diff --git a/compositor/src/effects.rs b/compositor/src/effects.rs index eaabf35..4f0a57e 100644 --- a/compositor/src/effects.rs +++ b/compositor/src/effects.rs @@ -51,6 +51,14 @@ pub(crate) fn resolve_material(role: SemanticRole, style: MaterialStyle, opacity ResolvedMaterial { style, opacity, shadow } } +#[derive(Debug)] +struct PendingPresentation { + role: SemanticRole, + regions: Vec<[f64; 5]>, + opacity: u32, + region_opacities: Vec, +} + #[derive(Debug)] struct SurfaceEffectsState { attached: AtomicBool, @@ -58,6 +66,7 @@ struct SurfaceEffectsState { role: Mutex>, regions: Mutex>>, opacity: Mutex, + pending: Mutex>, region_opacities: Mutex>, presentation_supported: AtomicBool, dismissing: AtomicBool, @@ -71,12 +80,26 @@ impl SurfaceEffectsState { role: Mutex::new(None), regions: Mutex::new(None), opacity: Mutex::new(1000), + pending: Mutex::new(None), region_opacities: Mutex::new(Vec::new()), presentation_supported: AtomicBool::new(false), dismissing: AtomicBool::new(false), } } + fn commit_presentation(&self) { + let Some(pending) = self.pending.lock().unwrap().take() else { + return; + }; + *self.role.lock().unwrap() = Some(pending.role); + *self.regions.lock().unwrap() = Some(pending.regions); + *self.region_opacities.lock().unwrap() = pending.region_opacities; + if !self.dismissing.load(Ordering::Acquire) { + *self.opacity.lock().unwrap() = pending.opacity; + } + self.generation.fetch_add(1, Ordering::Release); + } + fn set_role(&self, role: Option) -> bool { let mut current = self.role.lock().unwrap(); if *current != role { @@ -103,7 +126,7 @@ impl SurfaceEffectsUserData { } pub(crate) fn init_global(display: &DisplayHandle) { - display.create_global::(4, ()); + display.create_global::(5, ()); display.create_global::(1, ()); } @@ -185,6 +208,14 @@ impl Dispatch for Ferese { } } +pub(crate) fn commit_presentation(surface: &WlSurface) { + with_states(surface, |states| { + if let Some(effects) = states.data_map.get::() { + effects.commit_presentation(); + } + }); +} + pub(crate) fn surface_regions(surface: &WlSurface) -> Option> { with_states(surface, |states| { states @@ -330,6 +361,30 @@ impl Dispatch for Ferese { }; match request { + ferese_surface_effects_v1::Request::SetPresentation { + role, + regions, + opacity, + region_opacities, + } => { + let (Some(role), Some(regions), Some(region_opacities)) = ( + decode_role(role), + decode_regions(®ions), + decode_region_opacities(®ion_opacities), + ) else { + return; + }; + with_states(&surface, |states| { + if let Some(effects) = states.data_map.get::() { + *effects.pending.lock().unwrap() = Some(PendingPresentation { + role, + regions, + opacity: opacity.min(1000), + region_opacities, + }); + } + }); + } ferese_surface_effects_v1::Request::SetOpacity { opacity } => { let changed = with_states(&surface, |states| { let Some(effects) = states.data_map.get::() else { @@ -352,15 +407,8 @@ impl Dispatch for Ferese { } } ferese_surface_effects_v1::Request::SetRole { role } => { - let role = match role { - WEnum::Value(ferese_surface_effects_v1::Role::Panel) => SemanticRole::Panel, - WEnum::Value(ferese_surface_effects_v1::Role::PanelElevated) => SemanticRole::PanelElevated, - WEnum::Value(ferese_surface_effects_v1::Role::Popover) => SemanticRole::Popover, - WEnum::Value(ferese_surface_effects_v1::Role::Menu) => SemanticRole::Menu, - WEnum::Value(ferese_surface_effects_v1::Role::Notification) => SemanticRole::Notification, - WEnum::Value(ferese_surface_effects_v1::Role::Hud) => SemanticRole::Hud, - WEnum::Value(ferese_surface_effects_v1::Role::Modal) => SemanticRole::Modal, - WEnum::Unknown(_) | WEnum::Value(_) => return, + let Some(role) = decode_role(role) else { + return; }; if set_surface_role(&surface, Some(role)) { crate::backends::direct::render_surface(state, &surface); @@ -432,6 +480,19 @@ impl Dispatch for Ferese { } } +fn decode_role(role: WEnum) -> Option { + Some(match role { + WEnum::Value(ferese_surface_effects_v1::Role::Panel) => SemanticRole::Panel, + WEnum::Value(ferese_surface_effects_v1::Role::PanelElevated) => SemanticRole::PanelElevated, + WEnum::Value(ferese_surface_effects_v1::Role::Popover) => SemanticRole::Popover, + WEnum::Value(ferese_surface_effects_v1::Role::Menu) => SemanticRole::Menu, + WEnum::Value(ferese_surface_effects_v1::Role::Notification) => SemanticRole::Notification, + WEnum::Value(ferese_surface_effects_v1::Role::Hud) => SemanticRole::Hud, + WEnum::Value(ferese_surface_effects_v1::Role::Modal) => SemanticRole::Modal, + WEnum::Unknown(_) | WEnum::Value(_) => return None, + }) +} + fn set_surface_role(surface: &WlSurface, role: Option) -> bool { with_states(surface, |states| { states @@ -448,6 +509,7 @@ fn detach(surface: &WlSurface) -> bool { }; let role_changed = effects.set_role(None); effects.region_opacities.lock().unwrap().clear(); + effects.pending.lock().unwrap().take(); effects.dismissing.store(false, Ordering::Release); let mut opacity = effects.opacity.lock().unwrap(); let opacity_changed = *opacity != 1000; @@ -489,6 +551,38 @@ pub(crate) fn fade_dismissed_surface(surface: &WlSurface, opacity: f64) { mod tests { use super::*; + #[test] + fn presentation_waits_for_buffer_commit_and_keeps_dismissal_ownership() { + let state = SurfaceEffectsState::new(); + let queue = |opacity| PendingPresentation { + role: SemanticRole::Popover, + regions: vec![[1.25, 2.5, 100.0, 80.0, 12.0]], + opacity, + region_opacities: vec![500], + }; + *state.pending.lock().unwrap() = Some(queue(900)); + assert_eq!(*state.opacity.lock().unwrap(), 1000); + assert!(state.regions.lock().unwrap().is_none()); + assert_eq!(state.generation.load(Ordering::Acquire), 0); + // Several requests before one buffer commit present only the last. + *state.pending.lock().unwrap() = Some(queue(800)); + state.commit_presentation(); + assert_eq!(*state.opacity.lock().unwrap(), 800); + assert_eq!(state.regions.lock().unwrap().as_ref().unwrap()[0][0], 1.25); + assert_eq!(*state.region_opacities.lock().unwrap(), [500]); + assert_eq!(state.generation.load(Ordering::Acquire), 1); + state.commit_presentation(); + assert_eq!(state.generation.load(Ordering::Acquire), 1); + state.dismissing.store(true, Ordering::Release); + *state.pending.lock().unwrap() = Some(queue(1000)); + state.commit_presentation(); + assert_eq!( + *state.opacity.lock().unwrap(), + 800, + "client cannot restart compositor fade" + ); + } + #[test] fn region_opacities_validate_independent_fades() { let bytes: Vec<_> = [0u32, 500, 1000].into_iter().flat_map(u32::to_ne_bytes).collect(); diff --git a/compositor/src/handlers/compositor.rs b/compositor/src/handlers/compositor.rs index 5748a7f..074eec8 100644 --- a/compositor/src/handlers/compositor.rs +++ b/compositor/src/handlers/compositor.rs @@ -39,6 +39,7 @@ impl CompositorHandler for Ferese { self.capture_resize_before_commit(surface); self.capture_close_before_commit(surface); on_commit_buffer_handler::(surface); + crate::effects::commit_presentation(surface); if !is_sync_subsurface(surface) { let mut root = surface.clone(); while let Some(parent) = get_parent(&root) { diff --git a/protocols/ferese-effects-v1.xml b/protocols/ferese-effects-v1.xml index c2f15f7..2fd073e 100644 --- a/protocols/ferese-effects-v1.xml +++ b/protocols/ferese-effects-v1.xml @@ -22,7 +22,7 @@ DEALINGS IN THE SOFTWARE. - + This private global is visible only to clients granted the Ferese effects capability. Roles describe compositor-owned appearance policy and do not @@ -48,7 +48,7 @@ - + The role affects only compositor rendering. It does not change surface geometry, input, focus, stacking, or the surface's Wayland role. @@ -107,5 +107,19 @@ + + + Queue the role, regions, surface opacity and region opacities together. + They take effect on the next wl_surface.commit, with its content buffer. + Array formats and limits match set_regions and set_region_opacities. + Invalid arrays or roles leave the pending state unchanged. Surface opacity + is clamped to 1000. The last valid request before a commit wins. + Compositor-owned dismissal retains control of surface opacity. + + + + + + diff --git a/scripts/tests/test_shell_motion_isolated.py b/scripts/tests/test_shell_motion_isolated.py index f4fb71c..bb37b45 100644 --- a/scripts/tests/test_shell_motion_isolated.py +++ b/scripts/tests/test_shell_motion_isolated.py @@ -119,10 +119,10 @@ class ShellMotionTest(unittest.TestCase): # Clock/status updates still redraw settled surfaces. Count # commits: Iced can request two callbacks for one redraw. self.assertLessEqual(idle_frames, 12, "settled toast kept requesting animation frames") - material_updates = log_path.read_text().count('.set_region_opacities(') + material_updates = log_path.read_text().count('.set_presentation(') dbus("CloseNotification", notice) wait_for(lambda: destroyed(surface), timeout=5) - self.assertGreater(log_path.read_text().count('.set_region_opacities(') - material_updates, 5, + self.assertGreater(log_path.read_text().count('.set_presentation(') - material_updates, 5, "notification materials did not follow their fade") print(f"Toast: {opening_frames} opening callback requests, {idle_frames / 2:g} settled commits/s; destroyed", flush=True) diff --git a/shell/ferese-shell/src/notification_ui.rs b/shell/ferese-shell/src/notification_ui.rs index bbd4db6..c6fd5af 100644 --- a/shell/ferese-shell/src/notification_ui.rs +++ b/shell/ferese-shell/src/notification_ui.rs @@ -524,12 +524,14 @@ impl FereseShell { if let Some(surface) = &self.notification_surface && let Some(effects) = &surface.effects { - let _ = effects.set_regions(&surface.regions.lock().unwrap()); - let _ = effects.set_region_opacities( + let _ = effects.set_presentation( + &surface.regions.lock().unwrap(), + 1.0, self.notifications .popup_groups_at(now) .iter() .map(|(_, opacity, _)| *opacity), + ferese_surface_effects_v1::Role::Popover, ); } }, diff --git a/shell/ferese-shell/src/status_ui/lifecycle.rs b/shell/ferese-shell/src/status_ui/lifecycle.rs index ff763b8..cd702fb 100644 --- a/shell/ferese-shell/src/status_ui/lifecycle.rs +++ b/shell/ferese-shell/src/status_ui/lifecycle.rs @@ -145,9 +145,6 @@ impl FereseShell { pub fn animate_menu(&mut self) -> Task { if let Some(menu) = &self.menu { - if let Some(effects) = &menu.effects { - let _ = effects.set_opacity(menu.progress().clamp(0.0, 1.0)); - } if menu.motion.closing() && !menu.animating() { return self.destroy_menu(); } diff --git a/shell/ferese-shell/src/status_ui/mod.rs b/shell/ferese-shell/src/status_ui/mod.rs index d1f00e8..91711f0 100644 --- a/shell/ferese-shell/src/status_ui/mod.rs +++ b/shell/ferese-shell/src/status_ui/mod.rs @@ -120,6 +120,7 @@ impl OpenMenu { super::EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed); } + #[cfg(test)] pub fn progress(&self) -> f32 { self.motion.progress() } @@ -138,8 +139,12 @@ impl FereseShell { |now| menu.motion.frame_active(now), |now| { if let Some(effects) = &menu.effects { - let _ = effects.set_regions(&menu.regions.lock().unwrap()); - let _ = effects.set_opacity(menu.motion.progress_at(now).clamp(0.0, 1.0)); + let _ = effects.set_presentation( + &menu.regions.lock().unwrap(), + menu.motion.progress_at(now), + [], + super::ferese_surface_effects_v1::Role::Popover, + ); } }, |now| { diff --git a/shell/ferese-shell/src/surfaces.rs b/shell/ferese-shell/src/surfaces.rs index fca08e7..0d84a62 100644 --- a/shell/ferese-shell/src/surfaces.rs +++ b/shell/ferese-shell/src/surfaces.rs @@ -333,9 +333,6 @@ impl FereseShell { } _ => {} } - if let Some(effects) = &menu.effects { - let _ = effects.set_opacity(menu.progress()); - } } Task::none() } @@ -488,7 +485,7 @@ pub(super) struct EffectsBinding { _queue: EventQueue, regions: std::cell::RefCell>>, opacity: std::cell::Cell>, - region_opacities: std::cell::RefCell>, + presentation: std::cell::RefCell, u32, Vec, ferese_surface_effects_v1::Role)>>, } impl EffectsBinding { @@ -512,7 +509,7 @@ impl EffectsBinding { let connection = Connection::from_backend(backend); let (globals, queue) = registry_queue_init::(&connection)?; let qh = queue.handle(); - let manager = globals.bind::(&qh, 4..=4, ())?; + let manager = globals.bind::(&qh, 5..=5, ())?; let effects = manager.get_surface_effects(surface, &qh, ()); let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32; @@ -531,24 +528,33 @@ impl EffectsBinding { _queue: queue, regions: Default::default(), opacity: std::cell::Cell::new(Some(opacity)), - region_opacities: Default::default(), + presentation: Default::default(), }) } - pub(super) fn set_region_opacities( + pub(super) fn set_presentation( &self, - values: impl IntoIterator, + regions: &[[f32; 5]], + opacity: f32, + region_opacities: impl IntoIterator, + role: ferese_surface_effects_v1::Role, ) -> Result<(), Box> { - let values: Vec = values + let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32; + let values: Vec = region_opacities .into_iter() .take(32) .map(|value| (value.clamp(0.0, 1.0) * 1000.0).round() as u32) .collect(); - if *self.region_opacities.borrow() != values { - self.surface - .set_region_opacities(values.iter().flat_map(|value| value.to_ne_bytes()).collect()); + let next = (regions.to_vec(), opacity, values, role); + if self.presentation.borrow().as_ref() != Some(&next) { + self.surface.set_presentation( + role, + encode_regions(regions), + opacity, + next.2.iter().flat_map(|value| value.to_ne_bytes()).collect(), + ); self.connection.flush()?; - *self.region_opacities.borrow_mut() = values; + *self.presentation.borrow_mut() = Some(next); } Ok(()) } diff --git a/shell/ferese-shell/src/system_modal.rs b/shell/ferese-shell/src/system_modal.rs index c32ba6f..228b7a3 100644 --- a/shell/ferese-shell/src/system_modal.rs +++ b/shell/ferese-shell/src/system_modal.rs @@ -291,18 +291,6 @@ impl FereseShell { } } - pub(super) fn update_power_materials(&self) { - if let Some(modal) = &self.system_modal { - for entry in &modal.surfaces { - if let Some(effects) = &entry.effects { - let regions = entry.regions.lock().unwrap().clone(); - let _ = effects.set_material_regions(®ions, ferese_surface_effects_v1::Role::Modal); - let _ = effects.set_opacity(modal.motion.progress().clamp(0.0, 1.0)); - } - } - } - } - pub(super) fn close_system_modal(&mut self) -> Task { if self.system_modal.as_ref().is_some_and(SystemModal::is_display_mode) { if self.display_mode.busy { @@ -339,7 +327,6 @@ impl FereseShell { { return self.destroy_system_modal(false); } - self.update_power_materials(); Task::none() } @@ -483,9 +470,12 @@ impl FereseShell { |now| modal.motion.frame_active(now), |now| { if let Some(effects) = &surface.effects { - let _ = effects - .set_material_regions(&surface.regions.lock().unwrap(), ferese_surface_effects_v1::Role::Modal); - let _ = effects.set_opacity(modal.motion.progress_at(now).clamp(0.0, 1.0)); + let _ = effects.set_presentation( + &surface.regions.lock().unwrap(), + modal.motion.progress_at(now), + [], + ferese_surface_effects_v1::Role::Modal, + ); } }, |now| {