Fix review findings across compositor and shell
This commit is contained in:
1 parent
5d9673aad3
commit
f7e5efb245
30 files changed
+1222
-181
No files matched your search
@@ -159,6 +159,7 @@ struct FereseShell {
|
||||
overview_active: bool,
|
||||
clock: String,
|
||||
outputs: Vec<OutputSurfaces>,
|
||||
effects_context: Option<std::rc::Rc<EffectsContext>>,
|
||||
notifications: notifications::Center,
|
||||
notification_surface: Option<notification_ui::NotificationSurface>,
|
||||
status_service: status::Service,
|
||||
@@ -348,6 +349,7 @@ impl cosmic::Application for FereseShell {
|
||||
overview_active: false,
|
||||
clock: clock::current_time(),
|
||||
outputs: Vec::new(),
|
||||
effects_context: None,
|
||||
};
|
||||
let wallpaper_task = match wallpaper {
|
||||
Some(receiver) => cosmic::task::future(async move {
|
||||
@@ -564,7 +566,7 @@ impl cosmic::Application for FereseShell {
|
||||
&& entry.id == id
|
||||
&& entry.effects.is_none()
|
||||
{
|
||||
match EffectsBinding::attach_role(&surface, None, 1.0) {
|
||||
match EffectsBinding::attach_role(&mut self.effects_context, &surface, None, 1.0) {
|
||||
Ok(binding) => entry.effects = Some(binding),
|
||||
Err(error) => eprintln!("ferese-shell: notification effects unavailable: {error}"),
|
||||
}
|
||||
@@ -573,7 +575,7 @@ impl cosmic::Application for FereseShell {
|
||||
if let Some(menu) = &mut self.menu
|
||||
&& menu.id == id
|
||||
{
|
||||
menu.prepare_surface(&surface);
|
||||
menu.prepare_surface(&mut self.effects_context, &surface);
|
||||
}
|
||||
}
|
||||
Err(error) => eprintln!("ferese-shell: native surface unavailable: {error}"),
|
||||
@@ -779,7 +781,7 @@ impl cosmic::Application for FereseShell {
|
||||
count: self.notifications.unread(),
|
||||
dnd: value,
|
||||
});
|
||||
return Task::none();
|
||||
return self.sync_hub_activity();
|
||||
}
|
||||
status::Action::Notifications => {
|
||||
return Task::batch([self.close_menu(), self.toggle_notification_history()]);
|
||||
@@ -1194,6 +1196,7 @@ mod tests {
|
||||
bar_margin_horizontal: 0,
|
||||
hidden: false,
|
||||
}],
|
||||
effects_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,6 +1213,76 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dnd_refreshes_activity_selection_without_waiting_for_another_event() {
|
||||
use cosmic::Application;
|
||||
use ferese_activities::{Data, Owner, Source, Store, Template, Value};
|
||||
let mut shell = shell_with_measured_panel();
|
||||
shell.config.panels[0].style = ferese_panel::PanelStyle::Morph;
|
||||
shell.config.panels[0].center.groups[0]
|
||||
.items
|
||||
.push(ferese_panel::Item::new(
|
||||
"test-activities",
|
||||
ferese_panel::ItemKind::Activities,
|
||||
));
|
||||
shell.outputs[0]
|
||||
.runtime
|
||||
.reset(&shell.config.panels[0], &shell.snapshot, Some("test"));
|
||||
shell.snapshot.outputs.push(OutputSnapshot {
|
||||
id: 1,
|
||||
name: "test".into(),
|
||||
active_workspace: 1,
|
||||
focused: true,
|
||||
});
|
||||
shell.notifications.ready = true;
|
||||
let mut store = Store::new(1);
|
||||
for n in 1..=2 {
|
||||
store
|
||||
.start(
|
||||
Owner { pid: n, birth: 1 },
|
||||
n.to_string(),
|
||||
Data {
|
||||
source: Source::Publisher,
|
||||
output: Some("test".into()),
|
||||
priority: 100,
|
||||
icon: "hammer".into(),
|
||||
title: format!("Task {n}"),
|
||||
value: Value::Indeterminate,
|
||||
template: Template::Progress,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
None,
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
shell.activities.snapshot = std::sync::Arc::new(ferese_ipc::activities::Snapshot {
|
||||
activities: store.entries().cloned().collect(),
|
||||
..Default::default()
|
||||
});
|
||||
shell.sync_activity_presentations();
|
||||
let selected = |shell: &FereseShell| {
|
||||
shell.outputs[0]
|
||||
.runtime
|
||||
.hub
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.borrow()
|
||||
.activities
|
||||
.selection
|
||||
.visible
|
||||
.len()
|
||||
};
|
||||
assert_eq!(selected(&shell), 2);
|
||||
drop(shell.update(Message::Control(status::Action::Dnd(true))));
|
||||
assert_eq!(selected(&shell), 1);
|
||||
drop(shell.update(Message::Control(status::Action::Dnd(false))));
|
||||
assert_eq!(selected(&shell), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reload_preserves_the_last_presented_theme_without_a_control_connection() {
|
||||
use cosmic::Application;
|
||||
|
||||
@@ -375,6 +375,23 @@ impl<M> Motion<'_, M> {
|
||||
}
|
||||
|
||||
impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
||||
fn a11y_nodes(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
tree: &widget::Tree,
|
||||
cursor: mouse::Cursor,
|
||||
) -> iced_accessibility::A11yTree {
|
||||
let mut nodes = self.content.as_widget().a11y_nodes(layout, tree, self.cursor(cursor));
|
||||
let offset = self.translation();
|
||||
for node in nodes.root_mut() {
|
||||
translate_accessible_node(node, offset);
|
||||
}
|
||||
for node in nodes.children_mut() {
|
||||
translate_accessible_node(node, offset);
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
self.content.as_widget().tag()
|
||||
}
|
||||
@@ -479,6 +496,30 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
||||
) {
|
||||
self.content.as_widget_mut().operate(tree, layout, renderer, operation);
|
||||
}
|
||||
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
tree: &'a mut widget::Tree,
|
||||
layout: Layout<'a>,
|
||||
renderer: &cosmic::Renderer,
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<cosmic::iced::advanced::overlay::Element<'a, M, Theme, cosmic::Renderer>> {
|
||||
let translation = translation + self.translation();
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.overlay(tree, layout, renderer, viewport, translation)
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_accessible_node(node: &mut iced_accessibility::A11yNode, offset: Vector) {
|
||||
if let Some(mut bounds) = node.node().bounds() {
|
||||
bounds.x0 += f64::from(offset.x);
|
||||
bounds.x1 += f64::from(offset.x);
|
||||
bounds.y0 += f64::from(offset.y);
|
||||
bounds.y1 += f64::from(offset.y);
|
||||
node.node_mut().set_bounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
struct CollectRegions(Vec<Rectangle>);
|
||||
@@ -527,6 +568,139 @@ impl widget::Operation for CollectRegions {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn motion_preserves_accessible_controls_and_translates_tooltip_overlays() {
|
||||
use super::*;
|
||||
use cosmic::iced::advanced::{clipboard, renderer::Headless};
|
||||
use cosmic::iced::{Font, Pixels, Point};
|
||||
let renderer = cosmic::iced::futures::executor::block_on(cosmic::Renderer::new(
|
||||
Font::default(),
|
||||
Pixels(14.),
|
||||
Some("tiny-skia"),
|
||||
))
|
||||
.unwrap();
|
||||
let viewport = Rectangle::with_size(Size::new(400., 300.));
|
||||
for edge in [ferese_panel::Edge::Top, ferese_panel::Edge::Bottom] {
|
||||
let make_content = || {
|
||||
// The toolkit tooltip itself does not expose an accessibility
|
||||
// tree. A sibling button verifies our wrapper's forwarding.
|
||||
cosmic::widget::row::with_capacity(2)
|
||||
.push(cosmic::widget::tooltip(
|
||||
cosmic::widget::Space::new().width(100).height(40),
|
||||
cosmic::widget::text("Tooltip"),
|
||||
cosmic::widget::tooltip::Position::Bottom,
|
||||
))
|
||||
.push(
|
||||
cosmic::widget::button::custom(cosmic::widget::text("Action"))
|
||||
.name("Accessible action")
|
||||
.on_press(())
|
||||
.width(100)
|
||||
.height(40),
|
||||
)
|
||||
};
|
||||
let mut plain: Element<'_, ()> = make_content().into();
|
||||
let mut plain_tree = widget::Tree::new(&plain);
|
||||
let plain_node = plain.as_widget_mut().layout(
|
||||
&mut plain_tree,
|
||||
&renderer,
|
||||
&layout::Limits::new(Size::ZERO, viewport.size()),
|
||||
);
|
||||
let plain_a11y =
|
||||
plain
|
||||
.as_widget()
|
||||
.a11y_nodes(Layout::new(&plain_node), &plain_tree, mouse::Cursor::Unavailable);
|
||||
assert!(!plain_a11y.root().is_empty());
|
||||
let mut wrapped = animated_from_edge(make_content().into(), 0.5, Default::default(), 10., edge);
|
||||
let mut tree = widget::Tree::new(&wrapped);
|
||||
let node =
|
||||
wrapped
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree, &renderer, &layout::Limits::new(Size::ZERO, viewport.size()));
|
||||
let offset = if edge == ferese_panel::Edge::Top { -2. } else { 2. };
|
||||
let accessible = wrapped
|
||||
.as_widget()
|
||||
.a11y_nodes(Layout::new(&node), &tree, mouse::Cursor::Unavailable);
|
||||
assert_eq!(accessible.root().len(), plain_a11y.root().len());
|
||||
for (actual, expected) in accessible
|
||||
.root()
|
||||
.iter()
|
||||
.chain(accessible.children())
|
||||
.zip(plain_a11y.root().iter().chain(plain_a11y.children()))
|
||||
{
|
||||
assert_eq!(actual.node().label(), expected.node().label());
|
||||
if let Some(bounds) = expected.node().bounds() {
|
||||
let actual = actual.node().bounds().unwrap();
|
||||
assert_eq!(actual.x0, bounds.x0);
|
||||
assert_eq!(actual.y0, bounds.y0 + offset);
|
||||
}
|
||||
}
|
||||
let cursor = mouse::Cursor::Available(Point::new(50., 20.));
|
||||
let event = Event::Mouse(mouse::Event::CursorMoved {
|
||||
position: Point::new(50., 20.),
|
||||
});
|
||||
let mut messages = Vec::new();
|
||||
plain.as_widget_mut().update(
|
||||
&mut plain_tree,
|
||||
&event,
|
||||
Layout::new(&plain_node),
|
||||
cursor,
|
||||
&renderer,
|
||||
&mut clipboard::Null,
|
||||
&mut Shell::new(&mut messages),
|
||||
&viewport,
|
||||
);
|
||||
wrapped.as_widget_mut().update(
|
||||
&mut tree,
|
||||
&event,
|
||||
Layout::new(&node),
|
||||
cursor,
|
||||
&renderer,
|
||||
&mut clipboard::Null,
|
||||
&mut Shell::new(&mut messages),
|
||||
&viewport,
|
||||
);
|
||||
let mut plain_overlay = plain
|
||||
.as_widget_mut()
|
||||
.overlay(
|
||||
&mut plain_tree,
|
||||
Layout::new(&plain_node),
|
||||
&renderer,
|
||||
&viewport,
|
||||
Vector::new(40., 80.),
|
||||
)
|
||||
.expect("tooltip opens");
|
||||
let mut overlay = wrapped
|
||||
.as_widget_mut()
|
||||
.overlay(
|
||||
&mut tree,
|
||||
Layout::new(&node),
|
||||
&renderer,
|
||||
&viewport,
|
||||
Vector::new(40., 80.),
|
||||
)
|
||||
.expect("motion forwards tooltip");
|
||||
let plain_layout = plain_overlay.as_overlay_mut().layout(&renderer, viewport.size());
|
||||
let overlay_layout = overlay.as_overlay_mut().layout(&renderer, viewport.size());
|
||||
fn tooltip_bounds(node: &layout::Node) -> Rectangle {
|
||||
// Tooltip and row each return an overlay group; descend past
|
||||
// full-viewport group nodes to the positioned tooltip root.
|
||||
let mut node = node;
|
||||
while let [child] = node.children() {
|
||||
if node.bounds().position() != cosmic::iced::Point::ORIGIN {
|
||||
break;
|
||||
}
|
||||
node = child;
|
||||
}
|
||||
node.bounds()
|
||||
}
|
||||
let plain_bounds = tooltip_bounds(&plain_layout);
|
||||
let bounds = tooltip_bounds(&overlay_layout);
|
||||
assert!(plain_bounds.y > 0.);
|
||||
assert_eq!(bounds.x, plain_bounds.x);
|
||||
assert_eq!(bounds.y, plain_bounds.y + offset as f32);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_bounce_spring_finishes_popup_close() {
|
||||
let settings = super::Settings {
|
||||
|
||||
@@ -703,6 +703,32 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dnd_immediately_removes_and_restores_a_persistent_bridged_notice() {
|
||||
use cosmic::Application;
|
||||
let mut app = crate::tests::shell_with_measured_panel();
|
||||
app.snapshot.outputs.push(crate::control::OutputSnapshot {
|
||||
id: 4,
|
||||
name: "test".into(),
|
||||
active_workspace: 1,
|
||||
focused: true,
|
||||
});
|
||||
let (center, _commands) = fixture();
|
||||
app.notifications = center;
|
||||
app.config.activities.sources.notification_bridge = true;
|
||||
app.notifications.configure_activity_bridge(true);
|
||||
let bar = app.outputs[0].bar;
|
||||
let mut persistent = notice(1);
|
||||
persistent.timeout = None;
|
||||
drop(app.update(crate::Message::NotificationEvent(Event::Notice(persistent))));
|
||||
assert!(app.activity_available(bar));
|
||||
drop(app.update(crate::Message::Control(crate::status::Action::Dnd(true))));
|
||||
assert!(!app.activity_available(bar));
|
||||
assert!(app.notifications.entries[0].live, "DND must not dismiss the notice");
|
||||
drop(app.update(crate::Message::Control(crate::status::Action::Dnd(false))));
|
||||
assert!(app.activity_available(bar));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_creates_an_activity_and_removes_it_on_expiry_and_dismissal() {
|
||||
use cosmic::Application;
|
||||
|
||||
@@ -137,9 +137,13 @@ impl PopoverAnchor {
|
||||
}
|
||||
|
||||
impl OpenMenu {
|
||||
pub(super) fn prepare_surface(&mut self, surface: &wl_surface::WlSurface) {
|
||||
pub(super) fn prepare_surface(
|
||||
&mut self,
|
||||
context: &mut Option<std::rc::Rc<EffectsContext>>,
|
||||
surface: &wl_surface::WlSurface,
|
||||
) {
|
||||
if self.effects.is_none() {
|
||||
match EffectsBinding::attach_role(surface, None, 0.0) {
|
||||
match EffectsBinding::attach_role(context, surface, None, 0.0) {
|
||||
Ok(binding) => self.effects = Some(binding),
|
||||
Err(error) => eprintln!("ferese-shell: popover material unavailable: {error}"),
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ use super::{
|
||||
wayland, window, wl_output, wl_registry, wl_surface,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(super) fn panel_placement(edge: ferese_panel::Edge, margin: i32, side: i32) -> (Anchor, IcedMargin) {
|
||||
let (anchor, top, bottom) = match edge {
|
||||
ferese_panel::Edge::Top => (Anchor::TOP, margin, 0),
|
||||
@@ -410,7 +413,7 @@ impl FereseShell {
|
||||
return self.sync_notification_surface();
|
||||
}
|
||||
wayland::PopupEvent::Configured { .. } => {
|
||||
menu.prepare_surface(&surface);
|
||||
menu.prepare_surface(&mut self.effects_context, &surface);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -438,7 +441,7 @@ impl FereseShell {
|
||||
if let Some(menu) = &mut self.menu
|
||||
&& menu.effects.is_none()
|
||||
{
|
||||
menu.prepare_surface(&surface);
|
||||
menu.prepare_surface(&mut self.effects_context, &surface);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -457,7 +460,7 @@ impl FereseShell {
|
||||
let entry = self.notification_surface.as_mut().unwrap();
|
||||
|
||||
if entry.effects.is_none() {
|
||||
match EffectsBinding::attach_role(&surface, None, 1.0) {
|
||||
match EffectsBinding::attach_role(&mut self.effects_context, &surface, None, 1.0) {
|
||||
Ok(binding) => entry.effects = Some(binding),
|
||||
Err(error) => {
|
||||
eprintln!("ferese-shell: notification effects unavailable: {error}")
|
||||
@@ -493,9 +496,9 @@ impl FereseShell {
|
||||
|
||||
let binding = if islands {
|
||||
// Wait for measured island bounds rather than briefly painting the full bar.
|
||||
EffectsBinding::attach_role(surface, None, 1.0)
|
||||
EffectsBinding::attach_role(&mut self.effects_context, surface, None, 1.0)
|
||||
} else {
|
||||
EffectsBinding::attach(surface, !hidden)
|
||||
EffectsBinding::attach(&mut self.effects_context, surface, !hidden)
|
||||
};
|
||||
|
||||
match binding {
|
||||
@@ -585,11 +588,28 @@ fn validate_region_counts(regions: usize, opacities: usize) -> Result<(), std::i
|
||||
|
||||
type EffectPresentation = (Vec<[f32; 5]>, u32, Vec<u32>, ferese_surface_effects_v1::Role);
|
||||
|
||||
// Keep one registry on the shell connection: wl_registry has no destructor on
|
||||
// the protocol versions we support, so creating one per popup leaks objects.
|
||||
pub(super) struct EffectsContext {
|
||||
connection: Connection,
|
||||
manager: Option<FereseEffectsManagerV1>,
|
||||
_globals: wayland_client::globals::GlobalList,
|
||||
queue: EventQueue<EffectsState>,
|
||||
}
|
||||
|
||||
impl Drop for EffectsContext {
|
||||
fn drop(&mut self) {
|
||||
if let Some(manager) = &self.manager {
|
||||
manager.destroy();
|
||||
}
|
||||
let _ = self.connection.flush();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct EffectsBinding {
|
||||
connection: Connection,
|
||||
_manager: FereseEffectsManagerV1,
|
||||
_context: std::rc::Rc<EffectsContext>,
|
||||
pub(super) surface: FereseSurfaceEffectsV1,
|
||||
_queue: EventQueue<EffectsState>,
|
||||
regions: std::cell::RefCell<Option<Vec<[f32; 5]>>>,
|
||||
opacity: std::cell::Cell<Option<u32>>,
|
||||
presentation: std::cell::RefCell<Option<EffectPresentation>>,
|
||||
@@ -597,8 +617,13 @@ pub(super) struct EffectsBinding {
|
||||
}
|
||||
|
||||
impl EffectsBinding {
|
||||
pub(super) fn attach(surface: &wl_surface::WlSurface, visible: bool) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
pub(super) fn attach(
|
||||
context: &mut Option<std::rc::Rc<EffectsContext>>,
|
||||
surface: &wl_surface::WlSurface,
|
||||
visible: bool,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
Self::attach_role(
|
||||
context,
|
||||
surface,
|
||||
visible.then_some(ferese_surface_effects_v1::Role::Panel),
|
||||
if visible { 1.0 } else { 0.0 },
|
||||
@@ -606,6 +631,7 @@ impl EffectsBinding {
|
||||
}
|
||||
|
||||
pub(super) fn attach_role(
|
||||
context: &mut Option<std::rc::Rc<EffectsContext>>,
|
||||
surface: &wl_surface::WlSurface,
|
||||
role: Option<ferese_surface_effects_v1::Role>,
|
||||
opacity: f32,
|
||||
@@ -615,30 +641,50 @@ impl EffectsBinding {
|
||||
.upgrade()
|
||||
.ok_or("libcosmic Wayland connection is no longer alive")?;
|
||||
let connection = Connection::from_backend(backend);
|
||||
let (globals, queue) = registry_queue_init::<EffectsState>(&connection)?;
|
||||
let qh = queue.handle();
|
||||
let manager = globals.bind::<FereseEffectsManagerV1, _, _>(&qh, 6..=6, ())?;
|
||||
let effects = manager.get_surface_effects(surface, &qh, ());
|
||||
// Native handles can create distinct borrowed Backend wrappers around
|
||||
// the same wl_display. Backend equality compares wrappers, not displays.
|
||||
if context
|
||||
.as_ref()
|
||||
.is_none_or(|context| context.connection.backend().display_ptr() != connection.backend().display_ptr())
|
||||
{
|
||||
let (globals, queue) = registry_queue_init::<EffectsState>(&connection)?;
|
||||
// Cache the registry even when this compositor lacks our extension.
|
||||
let manager = globals
|
||||
.bind::<FereseEffectsManagerV1, _, _>(&queue.handle(), 6..=6, ())
|
||||
.ok();
|
||||
*context = Some(std::rc::Rc::new(EffectsContext {
|
||||
connection: connection.clone(),
|
||||
manager,
|
||||
_globals: globals,
|
||||
queue,
|
||||
}));
|
||||
}
|
||||
let context = context.as_ref().unwrap().clone();
|
||||
let effects = context
|
||||
.manager
|
||||
.as_ref()
|
||||
.ok_or("Ferese effects protocol unavailable")?
|
||||
.get_surface_effects(surface, &context.queue.handle(), ());
|
||||
|
||||
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
|
||||
effects.set_opacity(opacity);
|
||||
if let Some(role) = role {
|
||||
effects.set_role(role);
|
||||
} else {
|
||||
effects.clear_role();
|
||||
}
|
||||
connection.flush()?;
|
||||
|
||||
Ok(Self {
|
||||
// Own the protocol object before any fallible operation, including flush.
|
||||
let binding = Self {
|
||||
connection,
|
||||
_manager: manager,
|
||||
_context: context,
|
||||
surface: effects,
|
||||
_queue: queue,
|
||||
regions: Default::default(),
|
||||
opacity: std::cell::Cell::new(Some(opacity)),
|
||||
presentation: Default::default(),
|
||||
panel_presentation: Default::default(),
|
||||
})
|
||||
};
|
||||
binding.surface.set_opacity(opacity);
|
||||
if let Some(role) = role {
|
||||
binding.surface.set_role(role);
|
||||
} else {
|
||||
binding.surface.clear_role();
|
||||
}
|
||||
binding.connection.flush()?;
|
||||
Ok(binding)
|
||||
}
|
||||
|
||||
pub(super) fn set_panel_presentation(
|
||||
@@ -744,6 +790,13 @@ impl EffectsBinding {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EffectsBinding {
|
||||
fn drop(&mut self) {
|
||||
self.surface.destroy();
|
||||
let _ = self.connection.flush();
|
||||
}
|
||||
}
|
||||
|
||||
struct EffectsState;
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for EffectsState {
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
|
||||
use ferese_protocols::effects::v1::server::{
|
||||
ferese_effects_manager_v1 as manager, ferese_surface_effects_v1 as effects,
|
||||
};
|
||||
use wayland_server::protocol::{wl_compositor, wl_surface};
|
||||
use wayland_server::{Client, DataInit, Dispatch, Display, DisplayHandle, GlobalDispatch, New};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Counts {
|
||||
managers: usize,
|
||||
manager_destroys: usize,
|
||||
surfaces: usize,
|
||||
effect_destroys: usize,
|
||||
}
|
||||
|
||||
struct Server(Arc<Mutex<Counts>>);
|
||||
|
||||
impl GlobalDispatch<wl_compositor::WlCompositor, ()> for Server {
|
||||
fn bind(
|
||||
_: &mut Self,
|
||||
_: &DisplayHandle,
|
||||
_: &Client,
|
||||
resource: New<wl_compositor::WlCompositor>,
|
||||
_: &(),
|
||||
init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
init.init(resource, ());
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_compositor::WlCompositor, ()> for Server {
|
||||
fn request(
|
||||
_: &mut Self,
|
||||
_: &Client,
|
||||
_: &wl_compositor::WlCompositor,
|
||||
request: wl_compositor::Request,
|
||||
_: &(),
|
||||
_: &DisplayHandle,
|
||||
init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
if let wl_compositor::Request::CreateSurface { id } = request {
|
||||
init.init(id, ());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_surface::WlSurface, ()> for Server {
|
||||
fn request(
|
||||
_: &mut Self,
|
||||
_: &Client,
|
||||
_: &wl_surface::WlSurface,
|
||||
_: wl_surface::Request,
|
||||
_: &(),
|
||||
_: &DisplayHandle,
|
||||
_: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalDispatch<manager::FereseEffectsManagerV1, ()> for Server {
|
||||
fn bind(
|
||||
state: &mut Self,
|
||||
_: &DisplayHandle,
|
||||
_: &Client,
|
||||
resource: New<manager::FereseEffectsManagerV1>,
|
||||
_: &(),
|
||||
init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
state.0.lock().unwrap().managers += 1;
|
||||
init.init(resource, ());
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<manager::FereseEffectsManagerV1, ()> for Server {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_: &Client,
|
||||
_: &manager::FereseEffectsManagerV1,
|
||||
request: manager::Request,
|
||||
_: &(),
|
||||
_: &DisplayHandle,
|
||||
init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
match request {
|
||||
manager::Request::GetSurfaceEffects { id, .. } => {
|
||||
state.0.lock().unwrap().surfaces += 1;
|
||||
init.init(id, ());
|
||||
}
|
||||
manager::Request::Destroy => state.0.lock().unwrap().manager_destroys += 1,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<effects::FereseSurfaceEffectsV1, ()> for Server {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_: &Client,
|
||||
_: &effects::FereseSurfaceEffectsV1,
|
||||
request: effects::Request,
|
||||
_: &(),
|
||||
_: &DisplayHandle,
|
||||
_: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
if matches!(request, effects::Request::Destroy) {
|
||||
state.0.lock().unwrap().effect_destroys += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wayland_client::delegate_noop!(super::EffectsState: ignore wayland_client::protocol::wl_compositor::WlCompositor);
|
||||
wayland_client::delegate_noop!(super::EffectsState: ignore wayland_client::protocol::wl_surface::WlSurface);
|
||||
|
||||
#[test]
|
||||
fn popup_effects_share_the_registry_and_destroy_every_protocol_object() {
|
||||
effects_lifecycle(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_effects_extension_reuses_the_registry_across_failed_attachments() {
|
||||
effects_lifecycle(false);
|
||||
}
|
||||
|
||||
fn effects_lifecycle(advertise: bool) {
|
||||
use wayland_client::protocol::wl_compositor::WlCompositor;
|
||||
let counts = Arc::new(Mutex::new(Counts::default()));
|
||||
let worker_counts = counts.clone();
|
||||
let (client, server) = std::os::unix::net::UnixStream::pair().unwrap();
|
||||
let (stop, stopped) = mpsc::channel::<()>();
|
||||
let worker = std::thread::spawn(move || {
|
||||
let mut display = Display::<Server>::new().unwrap();
|
||||
let mut handle = display.handle();
|
||||
handle.create_global::<Server, wl_compositor::WlCompositor, _>(1, ());
|
||||
if advertise {
|
||||
handle.create_global::<Server, manager::FereseEffectsManagerV1, _>(6, ());
|
||||
}
|
||||
handle.insert_client(server, Arc::new(())).unwrap();
|
||||
let mut state = Server(worker_counts);
|
||||
loop {
|
||||
display.dispatch_clients(&mut state).unwrap();
|
||||
display.flush_clients().unwrap();
|
||||
match stopped.recv_timeout(std::time::Duration::from_millis(1)) {
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
let connection = wayland_client::Connection::from_socket(client).unwrap();
|
||||
let (globals, queue) = wayland_client::globals::registry_queue_init::<super::EffectsState>(&connection).unwrap();
|
||||
let compositor = globals.bind::<WlCompositor, _, _>(&queue.handle(), 1..=1, ()).unwrap();
|
||||
let mut context = None;
|
||||
let mut first = None;
|
||||
for _ in 0..20 {
|
||||
let surface = compositor.create_surface(&queue.handle(), ());
|
||||
use wayland_client::Proxy;
|
||||
// Match window::run: each native callback lends the same display through
|
||||
// a fresh borrowed backend. Neither wrapper owns the actual display.
|
||||
let borrowed = wayland_client::Connection::from_backend(unsafe {
|
||||
wayland_client::backend::Backend::from_foreign_display(connection.backend().display_ptr())
|
||||
});
|
||||
assert_ne!(borrowed, connection);
|
||||
let id = unsafe {
|
||||
wayland_client::backend::ObjectId::from_ptr(
|
||||
wayland_client::protocol::wl_surface::WlSurface::interface(),
|
||||
surface.id().as_ptr(),
|
||||
)
|
||||
}
|
||||
.unwrap();
|
||||
let borrowed_surface = wayland_client::protocol::wl_surface::WlSurface::from_id(&borrowed, id).unwrap();
|
||||
let binding = super::EffectsBinding::attach_role(&mut context, &borrowed_surface, None, 1.);
|
||||
assert_eq!(binding.is_ok(), advertise);
|
||||
if let Some(first) = &first {
|
||||
assert!(std::rc::Rc::ptr_eq(first, context.as_ref().unwrap()));
|
||||
} else {
|
||||
first = context.clone();
|
||||
}
|
||||
// Parent surface lifetime does not release the effects protocol object.
|
||||
surface.destroy();
|
||||
drop(binding);
|
||||
}
|
||||
connection.roundtrip().unwrap();
|
||||
{
|
||||
let counts = counts.lock().unwrap();
|
||||
assert_eq!(counts.managers, usize::from(advertise));
|
||||
assert_eq!(counts.surfaces, if advertise { 20 } else { 0 });
|
||||
assert_eq!(counts.effect_destroys, counts.surfaces);
|
||||
assert_eq!(counts.manager_destroys, 0);
|
||||
}
|
||||
drop(first);
|
||||
drop(context);
|
||||
connection.roundtrip().unwrap();
|
||||
assert_eq!(counts.lock().unwrap().manager_destroys, usize::from(advertise));
|
||||
drop(stop);
|
||||
worker.join().unwrap();
|
||||
}
|
||||
@@ -290,7 +290,7 @@ impl FereseShell {
|
||||
return;
|
||||
};
|
||||
if entry.primary && entry.effects.is_none() {
|
||||
match EffectsBinding::attach_role(surface, None, 0.0) {
|
||||
match EffectsBinding::attach_role(&mut self.effects_context, surface, None, 0.0) {
|
||||
Ok(effects) => entry.effects = Some(effects),
|
||||
Err(error) => eprintln!("ferese-shell: system modal material unavailable: {error}"),
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user