Fix review findings across compositor and shell

This commit is contained in:
Samuel Onoja committed 2026-10-13 04:02:17 +01:00
1 parent 5d9673aad3
commit f7e5efb245
30 files changed
+1222 -181

No files matched your search

+10
View File
@@ -330,6 +330,7 @@ impl cosmic::Application for App {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::DragWindow => return self.core.drag(None),
Message::ReadRenderScale(id) => {
return cosmic::iced::window::scale_factor(id)
.map(|scale| cosmic::Action::App(Message::RenderScale(scale)));
@@ -1087,6 +1088,15 @@ mod tests {
.0
}
#[test]
fn header_drag_targets_the_main_window_and_handles_a_missing_window() {
let mut app = app();
app.core.set_main_window_id(None);
assert_eq!(app.update(Message::DragWindow).units(), 0);
app.core.set_main_window_id(Some(cosmic::iced::window::Id::unique()));
assert_eq!(app.update(Message::DragWindow).units(), 1);
}
fn shortcuts(count: usize) -> App {
let mut app = app();
app.page = Page::Shortcuts;
+4
View File
@@ -33,3 +33,7 @@ image-codec = { package = "image", version = "0.25.10", default-features = false
url = "2.5.8"
base64 = "0.22.1"
ureq = { version = "2.12.1", default-features = false, features = ["tls"] }
[dev-dependencies]
wayland-server.workspace = true
ferese-protocols = { path = "../../crates/ferese-protocols", features = ["server"] }
+76 -3
View File
@@ -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;
+174
View File
@@ -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 {
+26
View File
@@ -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;
+6 -2
View File
@@ -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}"),
}
+77 -24
View File
@@ -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 {
+196
View File
@@ -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();
}
+1 -1
View File
@@ -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}"),
}