From a367cf23115bba57762221892c5119cfbcb1ec38 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 17 Oct 2024 16:05:19 +0200 Subject: [PATCH] wayland: implement ext-tray-v1 --- docs/features.md | 1 + release-notes.md | 1 + src/compositor.rs | 2 + src/globals.rs | 6 +- src/ifs.rs | 1 + src/ifs/ext_tray_v1.rs | 109 +++++ src/ifs/wl_output/removed_output.rs | 2 +- src/ifs/wl_seat.rs | 46 +- src/ifs/wl_seat/pointer_owner.rs | 6 +- src/ifs/wl_surface.rs | 14 + src/ifs/wl_surface/ext_tray_item_v1.rs | 424 ++++++++++++++++++ src/ifs/wl_surface/xdg_surface.rs | 23 +- src/ifs/wl_surface/xdg_surface/xdg_popup.rs | 22 +- .../wp_drm_lease_device_v1/removed_device.rs | 2 +- src/ifs/xdg_positioner.rs | 36 +- src/renderer.rs | 7 + src/state.rs | 9 +- src/tasks/connector.rs | 18 +- src/tasks/drmdev.rs | 2 +- src/tree.rs | 6 +- src/tree/output.rs | 58 ++- src/tree/walker.rs | 15 + src/utils/numcell.rs | 10 + wire/ext_tray_item_v1.txt | 30 ++ wire/ext_tray_v1.txt | 7 + 25 files changed, 817 insertions(+), 40 deletions(-) create mode 100644 src/ifs/ext_tray_v1.rs create mode 100644 src/ifs/wl_surface/ext_tray_item_v1.rs create mode 100644 wire/ext_tray_item_v1.txt create mode 100644 wire/ext_tray_v1.txt diff --git a/docs/features.md b/docs/features.md index 053f70a8..cf38aa87 100644 --- a/docs/features.md +++ b/docs/features.md @@ -147,6 +147,7 @@ Jay supports the following wayland protocols: | ext_output_image_capture_source_manager_v1 | 1 | | | ext_session_lock_manager_v1 | 1 | Yes | | ext_transient_seat_manager_v1 | 1[^ts_rejected] | Yes | +| ext_tray_v1 | 1 | | | org_kde_kwin_server_decoration_manager | 1 | | | wl_compositor | 6 | | | wl_data_device_manager | 3 | | diff --git a/release-notes.md b/release-notes.md index 3040c00e..4747380c 100644 --- a/release-notes.md +++ b/release-notes.md @@ -11,6 +11,7 @@ - Fix screen sharing in zoom. - Implement wp-fifo-v1. - Implement wp-commit-timing-v1. +- Implement ext-tray-v1. # 1.6.0 (2024-09-25) diff --git a/src/compositor.rs b/src/compositor.rs index 61236341..027cb30d 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -586,6 +586,8 @@ fn create_dummy_output(state: &Rc) { flip_margin_ns: Default::default(), ext_copy_sessions: Default::default(), before_latch_event: Default::default(), + tray_start_rel: Default::default(), + tray_items: Default::default(), }); let dummy_workspace = Rc::new(WorkspaceNode { id: state.node_ids.next(), diff --git a/src/globals.rs b/src/globals.rs index fdb6d302..17357566 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -261,11 +261,11 @@ impl Globals { pub fn remove( &self, state: &State, - global: &T, + global: &Rc, ) -> Result<(), GlobalsError> { let _global = self.take(global.name(), true)?; global.remove(self); - let replacement = global.create_replacement(); + let replacement = global.clone().create_replacement(); assert_eq!(global.name(), replacement.name()); assert_eq!(global.interface().0, replacement.interface().0); self.removed.set(global.name(), replacement); @@ -360,5 +360,5 @@ pub trait WaylandGlobal: Global + 'static { } pub trait RemovableWaylandGlobal: WaylandGlobal { - fn create_replacement(&self) -> Rc; + fn create_replacement(self: Rc) -> Rc; } diff --git a/src/ifs.rs b/src/ifs.rs index 904078ae..c836f24b 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -8,6 +8,7 @@ pub mod ext_image_copy; pub mod ext_output_image_capture_source_manager_v1; pub mod ext_session_lock_manager_v1; pub mod ext_session_lock_v1; +pub mod ext_tray_v1; pub mod ipc; pub mod jay_compositor; pub mod jay_damage_tracking; diff --git a/src/ifs/ext_tray_v1.rs b/src/ifs/ext_tray_v1.rs new file mode 100644 index 00000000..0cb657ea --- /dev/null +++ b/src/ifs/ext_tray_v1.rs @@ -0,0 +1,109 @@ +use { + crate::{ + client::{Client, ClientError}, + globals::{Global, GlobalName, RemovableWaylandGlobal}, + ifs::{ + wl_output::OutputGlobalOpt, + wl_surface::ext_tray_item_v1::{ExtTrayItemV1, ExtTrayItemV1Error}, + }, + leaks::Tracker, + object::{Object, Version}, + wire::{ext_tray_v1::*, ExtTrayV1Id}, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct ExtTrayV1Global { + pub name: GlobalName, + pub output: Rc, +} + +pub struct ExtTrayV1 { + pub id: ExtTrayV1Id, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, + pub output: Rc, +} + +impl ExtTrayV1Global { + fn bind_( + self: Rc, + id: ExtTrayV1Id, + client: &Rc, + version: Version, + ) -> Result<(), ExtTrayManagerV1Error> { + let obj = Rc::new(ExtTrayV1 { + id, + client: client.clone(), + tracker: Default::default(), + version, + output: self.output.clone(), + }); + track!(client, obj); + client.add_client_obj(&obj)?; + Ok(()) + } +} + +global_base!(ExtTrayV1Global, ExtTrayV1, ExtTrayManagerV1Error); + +impl Global for ExtTrayV1Global { + fn singleton(&self) -> bool { + false + } + + fn version(&self) -> u32 { + 1 + } +} + +simple_add_global!(ExtTrayV1Global); + +impl RemovableWaylandGlobal for ExtTrayV1Global { + fn create_replacement(self: Rc) -> Rc { + self + } +} + +impl ExtTrayV1RequestHandler for ExtTrayV1 { + type Error = ExtTrayManagerV1Error; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + Ok(()) + } + + fn get_tray_item(&self, req: GetTrayItem, _slf: &Rc) -> Result<(), Self::Error> { + let surface = self.client.lookup(req.surface)?; + let fs = Rc::new(ExtTrayItemV1::new( + req.id, + self.version, + &surface, + &self.output, + )); + track!(self.client, fs); + fs.install()?; + self.client.add_client_obj(&fs)?; + Ok(()) + } +} + +object_base! { + self = ExtTrayV1; + version = self.version; +} + +impl Object for ExtTrayV1 {} + +simple_add_obj!(ExtTrayV1); + +#[derive(Debug, Error)] +pub enum ExtTrayManagerV1Error { + #[error(transparent)] + ClientError(Box), + #[error(transparent)] + ExtTrayItemV1Error(#[from] ExtTrayItemV1Error), +} +efrom!(ExtTrayManagerV1Error, ClientError); diff --git a/src/ifs/wl_output/removed_output.rs b/src/ifs/wl_output/removed_output.rs index 67072aec..598335e9 100644 --- a/src/ifs/wl_output/removed_output.rs +++ b/src/ifs/wl_output/removed_output.rs @@ -50,7 +50,7 @@ impl Global for RemovedOutputGlobal { simple_add_global!(RemovedOutputGlobal); impl RemovableWaylandGlobal for WlOutputGlobal { - fn create_replacement(&self) -> Rc { + fn create_replacement(self: Rc) -> Rc { Rc::new(RemovedOutputGlobal { name: self.name }) } } diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index b4598f8f..679e5d80 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -23,6 +23,7 @@ pub mod zwp_virtual_keyboard_v1; use { crate::{ async_engine::SpawnedFuture, + backend::KeyState, client::{Client, ClientError, ClientId}, cursor_user::{CursorUser, CursorUserGroup, CursorUserOwner}, ei::ei_ifs::ei_seat::EiSeat, @@ -64,7 +65,10 @@ use { zwp_pointer_gesture_swipe_v1::ZwpPointerGestureSwipeV1, zwp_relative_pointer_v1::ZwpRelativePointerV1, }, - wl_surface::{dnd_icon::DndIcon, WlSurface}, + wl_surface::{ + dnd_icon::DndIcon, ext_tray_item_v1::ExtTrayItemV1, + xdg_surface::xdg_popup::XdgPopup, WlSurface, + }, xdg_toplevel_drag_v1::XdgToplevelDragV1, }, leaks::Tracker, @@ -81,9 +85,9 @@ use { rc_eq::rc_eq, smallmap::SmallMap, }, wire::{ - wl_seat::*, ExtIdleNotificationV1Id, WlDataDeviceId, WlKeyboardId, WlPointerId, - WlSeatId, WlTouchId, ZwlrDataControlDeviceV1Id, ZwpPrimarySelectionDeviceV1Id, - ZwpRelativePointerV1Id, ZwpTextInputV3Id, + wl_seat::*, ExtIdleNotificationV1Id, ExtTrayItemV1Id, WlDataDeviceId, WlKeyboardId, + WlPointerId, WlSeatId, WlTouchId, XdgPopupId, ZwlrDataControlDeviceV1Id, + ZwpPrimarySelectionDeviceV1Id, ZwpRelativePointerV1Id, ZwpTextInputV3Id, }, wire_ei::EiSeatId, xkbcommon::{DynKeyboardState, KeyboardState, KeymapId, XkbKeymap, XkbState}, @@ -201,6 +205,7 @@ pub struct WlSeatGlobal { ei_seats: CopyHashMap<(ClientId, EiSeatId), Rc>, ui_drag_highlight: Cell>, keyboard_node_serial: Cell, + tray_popups: CopyHashMap<(ClientId, ExtTrayItemV1Id, XdgPopupId), Rc>, } const CHANGE_CURSOR_MOVED: u32 = 1 << 0; @@ -273,6 +278,7 @@ impl WlSeatGlobal { tablet: Default::default(), ei_seats: Default::default(), ui_drag_highlight: Default::default(), + tray_popups: Default::default(), }); slf.pointer_cursor.set_owner(slf.clone()); let seat = slf.clone(); @@ -1042,7 +1048,37 @@ impl WlSeatGlobal { }); } - #[expect(dead_code)] + pub fn add_tray_item_popup(&self, item: &Rc, popup: &Rc) { + self.tray_popups + .set((item.client.id, item.id, popup.id), item.clone()); + } + + pub fn remove_tray_item_popup(&self, item: &Rc, popup: &Rc) { + self.tray_popups + .remove(&(item.client.id, item.id, popup.id)); + } + + fn handle_node_button( + self: &Rc, + node: Rc, + time_usec: u64, + button: u32, + state: KeyState, + serial: u64, + ) { + if self.tray_popups.is_not_empty() && state == KeyState::Pressed { + let id = node.node_tray_item().map(|t| (t.client.id, t.id)); + self.tray_popups.lock().retain(|_, item| { + let retain = Some((item.client.id, item.id)) == id; + if !retain { + item.destroy_popups(); + } + retain + }) + } + node.node_on_button(self, time_usec, button, state, serial); + } + pub fn handle_focus_request(self: &Rc, client: &Client, node: Rc, serial: u64) { let Some(max_serial) = client.focus_stealing_serial.get() else { return; diff --git a/src/ifs/wl_seat/pointer_owner.rs b/src/ifs/wl_seat/pointer_owner.rs index 8c9b4ad1..6d61eedf 100644 --- a/src/ifs/wl_seat/pointer_owner.rs +++ b/src/ifs/wl_seat/pointer_owner.rs @@ -336,7 +336,7 @@ impl PointerOwner for SimplePointerOwner { serial, })); pn.node_seat_state().add_pointer_grab(seat); - pn.node_on_button(seat, time_usec, button, state, serial); + seat.handle_node_button(pn, time_usec, button, state, serial); } fn axis_node(&self, seat: &Rc) -> Option> { @@ -448,9 +448,7 @@ impl PointerOwner for SimpleGrabPointerOwner { } } let serial = seat.state.next_serial(self.node.node_client().as_deref()); - self.node - .clone() - .node_on_button(seat, time_usec, button, state, serial); + seat.handle_node_button(self.node.clone(), time_usec, button, state, serial); } fn axis_node(&self, _seat: &Rc) -> Option> { diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index e19d5735..eae2731e 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -2,6 +2,7 @@ pub mod commit_timeline; pub mod cursor; pub mod dnd_icon; pub mod ext_session_lock_surface_v1; +pub mod ext_tray_item_v1; pub mod wl_subsurface; pub mod wp_alpha_modifier_surface_v1; pub mod wp_commit_timer_v1; @@ -46,6 +47,7 @@ use { commit_timeline::{ClearReason, CommitTimeline, CommitTimelineError}, cursor::CursorSurface, dnd_icon::DndIcon, + ext_tray_item_v1::ExtTrayItemV1, wl_subsurface::{PendingSubsurfaceData, SubsurfaceId, WlSubsurface}, wp_alpha_modifier_surface_v1::WpAlphaModifierSurfaceV1, wp_commit_timer_v1::WpCommitTimerV1, @@ -126,6 +128,7 @@ pub enum SurfaceRole { XSurface, ExtSessionLockSurface, InputPopup, + TrayItem, } impl SurfaceRole { @@ -140,6 +143,7 @@ impl SurfaceRole { SurfaceRole::XSurface => "xwayland surface", SurfaceRole::ExtSessionLockSurface => "ext_session_lock_surface", SurfaceRole::InputPopup => "input_popup_surface", + SurfaceRole::TrayItem => "tray_item", } } } @@ -412,6 +416,10 @@ trait SurfaceExt { ) -> Result<(), WlSurfaceError> { surface.pending.borrow_mut().consume_child(child, consume) } + + fn tray_item(self: Rc) -> Option> { + None + } } pub struct NoneSurfaceExt; @@ -450,6 +458,7 @@ struct PendingState { fifo_barrier_set: bool, fifo_barrier_wait: bool, commit_time: Option, + tray_item_ack_serial: Option, } struct AttachedSubsurfaceState { @@ -501,6 +510,7 @@ impl PendingState { opt!(content_type); opt!(alpha_multiplier); opt!(commit_time); + opt!(tray_item_ack_serial); { let (dx1, dy1) = self.offset; let (dx2, dy2) = mem::take(&mut next.offset); @@ -1721,6 +1731,10 @@ impl Node for WlSurface { self.toplevel.get() } + fn node_tray_item(&self) -> Option> { + self.ext.get().tray_item() + } + fn node_on_key( &self, seat: &WlSeatGlobal, diff --git a/src/ifs/wl_surface/ext_tray_item_v1.rs b/src/ifs/wl_surface/ext_tray_item_v1.rs new file mode 100644 index 00000000..9dbbea88 --- /dev/null +++ b/src/ifs/wl_surface/ext_tray_item_v1.rs @@ -0,0 +1,424 @@ +use { + crate::{ + client::{Client, ClientError, ClientId}, + ifs::{ + wl_output::OutputGlobalOpt, + wl_seat::{NodeSeatState, WlSeatGlobal}, + wl_surface::{ + xdg_surface::xdg_popup::{XdgPopup, XdgPopupParent}, + PendingState, SurfaceExt, SurfaceRole, WlSurface, WlSurfaceError, + }, + xdg_positioner::{ANCHOR_BOTTOM_LEFT, ANCHOR_BOTTOM_RIGHT}, + }, + leaks::Tracker, + object::{Object, Version}, + rect::Rect, + tree::{ + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, OutputNode, + StackedNode, + }, + utils::{ + copyhashmap::CopyHashMap, + hash_map_ext::HashMapExt, + linkedlist::{LinkedList, LinkedNode}, + numcell::NumCell, + }, + wire::{ext_tray_item_v1::*, ExtTrayItemV1Id, XdgPopupId}, + }, + std::{ + cell::{Cell, RefCell}, + rc::Rc, + }, + thiserror::Error, +}; + +tree_id!(TrayItemNodeId); +pub struct ExtTrayItemV1 { + pub id: ExtTrayItemV1Id, + node_id: TrayItemNodeId, + seat_state: NodeSeatState, + pub client: Rc, + visible: Cell, + pub surface: Rc, + pub tracker: Tracker, + version: Version, + output: Rc, + attached: Cell, + sent_serial: NumCell, + ack_serial: NumCell, + linked_node: Cell>>>, + popups: CopyHashMap>, + abs_pos: Cell, + pub rel_pos: Cell, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum FocusHint { + None, + OnDemand, + Immediate, +} + +impl ExtTrayItemV1 { + pub fn new( + id: ExtTrayItemV1Id, + version: Version, + surface: &Rc, + output: &Rc, + ) -> Self { + Self { + id, + node_id: surface.client.state.node_ids.next(), + seat_state: Default::default(), + client: surface.client.clone(), + visible: Cell::new(surface.client.state.root_visible()), + surface: surface.clone(), + tracker: Default::default(), + version, + output: output.clone(), + attached: Default::default(), + sent_serial: Default::default(), + ack_serial: Default::default(), + linked_node: Default::default(), + popups: Default::default(), + abs_pos: Default::default(), + rel_pos: Default::default(), + } + } + + pub fn install(self: &Rc) -> Result<(), ExtTrayItemV1Error> { + self.surface.set_role(SurfaceRole::TrayItem)?; + if self.surface.ext.get().is_some() { + return Err(ExtTrayItemV1Error::Exists); + } + self.surface.ext.set(self.clone()); + self.surface.set_visible(false); + if let Some(node) = self.output.node() { + self.surface.set_output(&node); + self.send_preferred_anchor(); + self.send_preferred_gravity(); + self.send_current_configure(); + } + Ok(()) + } + + pub fn set_position(&self, abs_pos: Rect, rel_pos: Rect) { + self.surface + .set_absolute_position(abs_pos.x1(), abs_pos.y1()); + self.rel_pos.set(rel_pos); + if self.abs_pos.replace(abs_pos) != abs_pos { + for popup in self.popups.lock().values() { + popup.popup.update_absolute_position(); + } + } + } + + pub fn send_current_configure(&self) { + let size = self.client.state.tray_icon_size().max(1); + self.send_configure_size(size, size); + self.send_configure(); + } + + fn send_configure_size(&self, width: i32, height: i32) { + self.client.event(ConfigureSize { + self_id: self.id, + width, + height, + }); + } + + fn send_preferred_anchor(&self) { + self.client.event(PreferredAnchor { + self_id: self.id, + anchor: ANCHOR_BOTTOM_LEFT, + }); + } + + fn send_preferred_gravity(&self) { + self.client.event(PreferredGravity { + self_id: self.id, + gravity: ANCHOR_BOTTOM_RIGHT, + }); + } + + fn send_configure(&self) { + self.client.event(Configure { + self_id: self.id, + serial: self.sent_serial.add_fetch(1), + }); + } + + pub fn destroy_popups(&self) { + for popup in self.popups.lock().drain_values() { + popup.popup.destroy_node(); + } + } + + pub fn destroy_node(&self) { + self.linked_node.take(); + self.attached.set(false); + self.destroy_popups(); + self.surface.destroy_node(); + self.seat_state.destroy_node(self); + self.client.state.tree_changed(); + if let Some(node) = self.output.node() { + node.update_tray_positions(); + } + } + + pub fn set_visible(&self, visible: bool) { + self.visible.set(visible); + let visible = visible && self.surface.buffer.is_some(); + self.surface.set_visible(visible); + if !visible { + self.destroy_popups(); + } + } +} + +impl ExtTrayItemV1RequestHandler for ExtTrayItemV1 { + type Error = ExtTrayItemV1Error; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + if self.popups.is_not_empty() { + return Err(ExtTrayItemV1Error::HasPopups); + } + self.destroy_node(); + self.surface.unset_ext(); + Ok(()) + } + + fn ack_configure(&self, req: AckConfigure, _slf: &Rc) -> Result<(), Self::Error> { + self.surface.pending.borrow_mut().tray_item_ack_serial = Some(req.serial); + Ok(()) + } + + fn get_popup(&self, req: GetPopup, slf: &Rc) -> Result<(), Self::Error> { + let popup = self.client.lookup(req.popup)?; + let seat = self.client.lookup(req.seat)?; + let seat = &seat.global; + let Some(serial) = self.client.map_serial(req.serial) else { + return Err(ExtTrayItemV1Error::InvalidSerial); + }; + if popup.parent.is_some() { + return Err(ExtTrayItemV1Error::PopupHasParent); + } + let focus = match req.keyboard_focus { + 0 => FocusHint::None, + 1 => FocusHint::OnDemand, + 2 => FocusHint::Immediate, + n => return Err(ExtTrayItemV1Error::InvalidFocusHint(n)), + }; + let Some(node) = self.output.node() else { + popup.destroy_node(); + return Ok(()); + }; + seat.add_tray_item_popup(slf, &popup); + let stack = self.client.state.root.stacked_above_layers.clone(); + popup.xdg.set_popup_stack(&stack); + popup.xdg.set_output(&node); + let user = Rc::new(Popup { + parent: slf.clone(), + popup: popup.clone(), + seat: seat.clone(), + serial, + focus, + stack, + stack_link: Default::default(), + }); + popup.parent.set(Some(user.clone())); + self.popups.set(popup.id, user); + Ok(()) + } +} + +struct Popup { + parent: Rc, + popup: Rc, + seat: Rc, + serial: u64, + focus: FocusHint, + stack: Rc>>, + stack_link: RefCell>>>, +} + +impl XdgPopupParent for Popup { + fn position(&self) -> Rect { + self.parent.abs_pos.get() + } + + fn remove_popup(&self) { + self.seat.remove_tray_item_popup(&self.parent, &self.popup); + self.parent.popups.remove(&self.popup.id); + } + + fn output(&self) -> Rc { + self.parent.surface.output.get() + } + + fn has_workspace_link(&self) -> bool { + false + } + + fn post_commit(&self) { + let mut dl = self.stack_link.borrow_mut(); + let surface = &self.popup.xdg.surface; + let state = &surface.client.state; + if surface.buffer.is_some() { + if dl.is_none() { + if self.parent.surface.visible.get() { + self.popup.set_visible(true); + *dl = Some(self.stack.add_last(self.popup.clone())); + state.tree_changed(); + if self.focus == FocusHint::Immediate { + self.seat.handle_focus_request( + &self.parent.client, + self.popup.xdg.surface.clone(), + self.serial, + ); + } + } else { + self.popup.destroy_node(); + } + } + } else { + if dl.take().is_some() { + drop(dl); + self.popup.set_visible(false); + self.popup.destroy_node(); + } + } + } + + fn tray_item(&self) -> Option> { + Some(self.parent.clone()) + } + + fn allow_popup_focus(&self) -> bool { + match self.focus { + FocusHint::None => false, + FocusHint::OnDemand => true, + FocusHint::Immediate => true, + } + } +} + +impl SurfaceExt for ExtTrayItemV1 { + fn before_apply_commit( + self: Rc, + pending: &mut PendingState, + ) -> Result<(), WlSurfaceError> { + if let Some(serial) = pending.tray_item_ack_serial.take() { + self.ack_serial.set(serial); + } + Ok(()) + } + + fn after_apply_commit(self: Rc) { + if self.surface.visible.get() { + if self.surface.buffer.is_none() { + self.destroy_node(); + } + } else { + if self.ack_serial.get() != self.sent_serial.get() { + return; + } + if self.surface.buffer.is_some() { + self.surface.set_visible(self.visible.get()); + if let Some(node) = self.output.node() { + if !self.attached.replace(true) { + let link = node.tray_items.add_last(self.clone()); + self.linked_node.set(Some(link)); + node.update_tray_positions(); + } + } + } + } + } + + fn extents_changed(&self) { + if self.surface.visible.get() { + self.client.state.tree_changed(); + } + } + + fn tray_item(self: Rc) -> Option> { + Some(self) + } +} + +impl Node for ExtTrayItemV1 { + fn node_id(&self) -> NodeId { + self.node_id.into() + } + + fn node_seat_state(&self) -> &NodeSeatState { + &self.seat_state + } + + fn node_visit(self: Rc, visitor: &mut dyn NodeVisitor) { + visitor.visit_tray_item(&self); + } + + fn node_visit_children(&self, visitor: &mut dyn NodeVisitor) { + self.surface.clone().node_visit(visitor); + } + + fn node_visible(&self) -> bool { + self.surface.visible.get() + } + + fn node_absolute_position(&self) -> Rect { + self.surface.node_absolute_position() + } + + fn node_find_tree_at( + &self, + x: i32, + y: i32, + tree: &mut Vec, + _usecase: FindTreeUsecase, + ) -> FindTreeResult { + self.surface.find_tree_at_(x, y, tree) + } + + fn node_client(&self) -> Option> { + Some(self.client.clone()) + } + + fn node_client_id(&self) -> Option { + Some(self.client.id) + } +} + +object_base! { + self = ExtTrayItemV1; + version = self.version; +} + +impl Object for ExtTrayItemV1 { + fn break_loops(&self) { + self.destroy_node(); + } +} + +simple_add_obj!(ExtTrayItemV1); + +#[derive(Debug, Error)] +pub enum ExtTrayItemV1Error { + #[error(transparent)] + ClientError(Box), + #[error("The surface already has a tray item role object")] + Exists, + #[error(transparent)] + WlSurfaceError(#[from] WlSurfaceError), + #[error("Popup already has a parent")] + PopupHasParent, + #[error("Surface still has popups")] + HasPopups, + #[error("The serial is not valid")] + InvalidSerial, + #[error("The focus hint {} is invalid", .0)] + InvalidFocusHint(u32), +} +efrom!(ExtTrayItemV1Error, ClientError); diff --git a/src/ifs/wl_surface/xdg_surface.rs b/src/ifs/wl_surface/xdg_surface.rs index 6acd8f00..dee84d6f 100644 --- a/src/ifs/wl_surface/xdg_surface.rs +++ b/src/ifs/wl_surface/xdg_surface.rs @@ -6,6 +6,7 @@ use { client::ClientError, ifs::{ wl_surface::{ + ext_tray_item_v1::ExtTrayItemV1, xdg_surface::{ xdg_popup::{XdgPopup, XdgPopupError, XdgPopupParent}, xdg_toplevel::{XdgToplevel, WM_CAPABILITIES_SINCE}, @@ -17,7 +18,7 @@ use { leaks::Tracker, object::Object, rect::Rect, - tree::{FindTreeResult, FoundNode, OutputNode, StackedNode, WorkspaceNode}, + tree::{FindTreeResult, FoundNode, Node, OutputNode, StackedNode, WorkspaceNode}, utils::{ clonecell::CloneCell, copyhashmap::CopyHashMap, @@ -138,6 +139,10 @@ impl XdgPopupParent for Popup { } } } + + fn tray_item(&self) -> Option> { + self.parent.clone().tray_item() + } } #[derive(Default, Debug)] @@ -174,6 +179,14 @@ pub trait XdgSurfaceExt: Debug { fn geometry_changed(&self) { // nothing } + + fn focus_node(&self) -> Option> { + None + } + + fn tray_item(&self) -> Option> { + None + } } impl XdgSurface { @@ -526,6 +539,14 @@ impl SurfaceExt for XdgSurface { fn extents_changed(&self) { self.update_extents(); } + + fn focus_node(&self) -> Option> { + self.ext.get()?.focus_node() + } + + fn tray_item(self: Rc) -> Option> { + self.ext.get()?.tray_item() + } } #[derive(Debug, Error)] diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index 7351dde9..14fe3ca5 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs @@ -5,7 +5,10 @@ use { fixed::Fixed, ifs::{ wl_seat::{tablet::TabletTool, NodeSeatState, WlSeatGlobal}, - wl_surface::xdg_surface::{XdgSurface, XdgSurfaceError, XdgSurfaceExt}, + wl_surface::{ + ext_tray_item_v1::ExtTrayItemV1, + xdg_surface::{XdgSurface, XdgSurfaceError, XdgSurfaceExt}, + }, xdg_positioner::{ XdgPositioned, XdgPositioner, CA_FLIP_X, CA_FLIP_Y, CA_RESIZE_X, CA_RESIZE_Y, CA_SLIDE_X, CA_SLIDE_Y, @@ -41,6 +44,12 @@ pub trait XdgPopupParent { fn output(&self) -> Rc; fn has_workspace_link(&self) -> bool; fn post_commit(&self); + fn tray_item(&self) -> Option> { + None + } + fn allow_popup_focus(&self) -> bool { + false + } } pub struct XdgPopup { @@ -392,6 +401,17 @@ impl XdgSurfaceExt for XdgPopup { fn extents_changed(&self) { self.xdg.surface.client.state.tree_changed(); } + + fn focus_node(&self) -> Option> { + if self.parent.get()?.allow_popup_focus() { + return Some(self.xdg.surface.clone()); + } + None + } + + fn tray_item(&self) -> Option> { + self.parent.get()?.tray_item() + } } #[derive(Debug, Error)] diff --git a/src/ifs/wp_drm_lease_device_v1/removed_device.rs b/src/ifs/wp_drm_lease_device_v1/removed_device.rs index ff4f4425..a529560c 100644 --- a/src/ifs/wp_drm_lease_device_v1/removed_device.rs +++ b/src/ifs/wp_drm_lease_device_v1/removed_device.rs @@ -64,7 +64,7 @@ impl Global for RemovedWpDrmLeaseDeviceV1Global { } impl RemovableWaylandGlobal for WpDrmLeaseDeviceV1Global { - fn create_replacement(&self) -> Rc { + fn create_replacement(self: Rc) -> Rc { Rc::new(RemovedWpDrmLeaseDeviceV1Global { name: self.name, bindings: Default::default(), diff --git a/src/ifs/xdg_positioner.rs b/src/ifs/xdg_positioner.rs index 4538536c..7a3a066e 100644 --- a/src/ifs/xdg_positioner.rs +++ b/src/ifs/xdg_positioner.rs @@ -13,15 +13,15 @@ use { const INVALID_INPUT: u32 = 0; -const NONE: u32 = 0; -const TOP: u32 = 1; -const BOTTOM: u32 = 2; -const LEFT: u32 = 3; -const RIGHT: u32 = 4; -const TOP_LEFT: u32 = 5; -const BOTTOM_LEFT: u32 = 6; -const TOP_RIGHT: u32 = 7; -const BOTTOM_RIGHT: u32 = 8; +pub const ANCHOR_NONE: u32 = 0; +pub const ANCHOR_TOP: u32 = 1; +pub const ANCHOR_BOTTOM: u32 = 2; +pub const ANCHOR_LEFT: u32 = 3; +pub const ANCHOR_RIGHT: u32 = 4; +pub const ANCHOR_TOP_LEFT: u32 = 5; +pub const ANCHOR_BOTTOM_LEFT: u32 = 6; +pub const ANCHOR_TOP_RIGHT: u32 = 7; +pub const ANCHOR_BOTTOM_RIGHT: u32 = 8; bitflags! { Edge: u32; @@ -34,15 +34,15 @@ bitflags! { impl Edge { fn from_enum(e: u32) -> Option { let s = match e { - NONE => Self::none(), - TOP => E_TOP, - BOTTOM => E_BOTTOM, - LEFT => E_LEFT, - RIGHT => E_RIGHT, - TOP_LEFT => E_TOP | E_LEFT, - BOTTOM_LEFT => E_BOTTOM | E_LEFT, - TOP_RIGHT => E_TOP | E_RIGHT, - BOTTOM_RIGHT => E_BOTTOM | E_RIGHT, + ANCHOR_NONE => Self::none(), + ANCHOR_TOP => E_TOP, + ANCHOR_BOTTOM => E_BOTTOM, + ANCHOR_LEFT => E_LEFT, + ANCHOR_RIGHT => E_RIGHT, + ANCHOR_TOP_LEFT => E_TOP | E_LEFT, + ANCHOR_BOTTOM_LEFT => E_BOTTOM | E_LEFT, + ANCHOR_TOP_RIGHT => E_TOP | E_RIGHT, + ANCHOR_BOTTOM_RIGHT => E_BOTTOM | E_RIGHT, _ => return None, }; Some(s) diff --git a/src/renderer.rs b/src/renderer.rs index e4f35f4a..1c7608fb 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -148,6 +148,13 @@ impl Renderer<'_> { ); } } + for item in output.tray_items.iter() { + if item.surface.buffer.is_some() { + let rect = item.rel_pos.get().move_(x, y); + let bounds = self.base.scale_rect(rect); + self.render_surface(&item.surface, rect.x1(), rect.y1(), Some(&bounds)); + } + } } if let Some(ws) = output.workspace.get() { self.render_workspace(&ws, x, y + th + 1); diff --git a/src/state.rs b/src/state.rs index e84b65dc..a3293d5c 100644 --- a/src/state.rs +++ b/src/state.rs @@ -586,7 +586,10 @@ impl State { self.globals.add_global(self, global) } - pub fn remove_global(&self, global: &T) -> Result<(), GlobalsError> { + pub fn remove_global( + &self, + global: &Rc, + ) -> Result<(), GlobalsError> { self.globals.remove(self, global) } @@ -1254,6 +1257,10 @@ impl State { } } } + + pub fn tray_icon_size(&self) -> i32 { + (self.theme.sizes.title_height.get() - 2).max(0) + } } #[derive(Debug, Error)] diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index bcec654d..8bcb4686 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -2,7 +2,10 @@ use { crate::{ backend::{Connector, ConnectorEvent, ConnectorId, MonitorInfo}, globals::GlobalName, - ifs::wl_output::{PersistentOutputState, WlOutputGlobal}, + ifs::{ + ext_tray_v1::ExtTrayV1Global, + wl_output::{PersistentOutputState, WlOutputGlobal}, + }, output_schedule::OutputSchedule, state::{ConnectorData, OutputData, State}, tree::{move_ws_to_output, OutputNode, OutputRenderData, WsMoveConfig}, @@ -146,6 +149,10 @@ impl ConnectorHandler { .state .eng .spawn("output schedule", schedule.clone().drive()); + let tray = Rc::new(ExtTrayV1Global { + name: self.state.globals.name(), + output: global.opt.clone(), + }); let on = Rc::new(OutputNode { id: self.state.node_ids.next(), workspaces: Default::default(), @@ -188,6 +195,8 @@ impl ConnectorHandler { flip_margin_ns: Default::default(), ext_copy_sessions: Default::default(), before_latch_event: Default::default(), + tray_start_rel: Default::default(), + tray_items: Default::default(), }); on.update_visible(); on.update_rects(); @@ -247,6 +256,7 @@ impl ConnectorHandler { config.connector_connected(self.id); } self.state.add_global(&global); + self.state.add_global(&tray); self.state.tree_changed(); on.update_presentation_type(); 'outer: loop { @@ -324,9 +334,13 @@ impl ConnectorHandler { for seat in self.state.globals.seats.lock().values() { seat.cursor_group().output_disconnected(&on, &target); } + for item in on.tray_items.iter() { + item.destroy_node(); + } self.state .remove_output_scale(on.global.persistent.scale.get()); - let _ = self.state.remove_global(&*global); + let _ = self.state.remove_global(&global); + let _ = self.state.remove_global(&tray); self.state.tree_changed(); self.state.damage(self.state.root.extents.get()); } diff --git a/src/tasks/drmdev.rs b/src/tasks/drmdev.rs index b629d632..a06ec9f2 100644 --- a/src/tasks/drmdev.rs +++ b/src/tasks/drmdev.rs @@ -74,7 +74,7 @@ impl DrvDevHandler { config.del_drm_dev(self.id); } self.data.lease_global.bindings.clear(); - let _ = self.state.remove_global(&*self.data.lease_global); + let _ = self.state.remove_global(&self.data.lease_global); self.data.handler.set(None); self.state.drm_devs.remove(&self.id); } diff --git a/src/tree.rs b/src/tree.rs index 1901af79..3800d7cf 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -13,7 +13,7 @@ use { wl_pointer::PendingScroll, Dnd, NodeSeatState, WlSeatGlobal, }, - wl_surface::WlSurface, + wl_surface::{ext_tray_item_v1::ExtTrayItemV1, WlSurface}, }, rect::Rect, renderer::Renderer, @@ -178,6 +178,10 @@ pub trait Node: 'static { None } + fn node_tray_item(&self) -> Option> { + None + } + // EVENT HANDLERS fn node_on_key( diff --git a/src/tree/output.rs b/src/tree/output.rs index 52cbd96a..9f33c3b4 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -19,6 +19,7 @@ use { }, wl_surface::{ ext_session_lock_surface_v1::ExtSessionLockSurfaceV1, + ext_tray_item_v1::ExtTrayItemV1, zwlr_layer_surface_v1::{ExclusiveSize, ZwlrLayerSurfaceV1}, SurfaceSendPreferredScaleVisitor, SurfaceSendPreferredTransformVisitor, }, @@ -94,6 +95,8 @@ pub struct OutputNode { pub ext_copy_sessions: CopyHashMap<(ClientId, ExtImageCopyCaptureSessionV1Id), Rc>, pub before_latch_event: EventSource, + pub tray_start_rel: Cell, + pub tray_items: LinkedList>, } #[derive(Copy, Clone, Debug, PartialEq)] @@ -418,6 +421,9 @@ impl OutputNode { if let Some(c) = self.workspace.get() { c.change_extents(&self.workspace_rect.get()); } + for item in self.tray_items.iter() { + item.send_current_configure(); + } } pub fn set_preferred_scale(self: &Rc, scale: Scale) { @@ -579,7 +585,7 @@ impl OutputNode { if let Some(scale) = scale { width = (width as f64 / scale).round() as _; } - let pos = output_width - width - 1; + let pos = self.tray_start_rel.get() - width - 1; status.tex_x = pos; } } @@ -714,6 +720,7 @@ impl OutputNode { let height = (y2 - y1).max(0); self.workspace_rect .set(Rect::new_sized_unchecked(x1, y1, width, height)); + self.update_tray_positions(); self.schedule_update_render_data(); } @@ -929,6 +936,9 @@ impl OutputNode { self.title_visible.set(lower_visible); set_layer_visible!(self.layers[0], lower_visible); set_layer_visible!(self.layers[1], lower_visible); + for item in self.tray_items.iter() { + item.set_visible(lower_visible); + } if let Some(ws) = self.workspace.get() { ws.set_visible(visible); } @@ -1164,6 +1174,37 @@ impl OutputNode { before: None, }); } + + pub fn update_tray_positions(self: &Rc) { + let th = self.state.theme.sizes.title_height.get(); + let rect = self.non_exclusive_rect.get(); + let output_width = rect.width(); + let mut right = output_width; + let mut have_any = false; + let icon_size = self.state.tray_icon_size(); + for item in self.tray_items.rev_iter() { + if item.surface.buffer.is_none() { + continue; + } + have_any = true; + right -= th; + let rel_pos = Rect::new_sized(right, 1, icon_size, icon_size).unwrap(); + let abs_pos = rel_pos.move_(rect.x1(), rect.y1()); + item.set_position(abs_pos, rel_pos); + } + if have_any { + right -= 2; + } + let prev_right = self.tray_start_rel.replace(right); + if prev_right != right { + { + let min = prev_right.min(right); + let rect = Rect::new_sized(rect.x1() + min, 0, output_width, th).unwrap(); + self.state.damage(rect); + } + self.schedule_update_render_data(); + } + } } pub struct OutputTitle { @@ -1228,6 +1269,9 @@ impl Node for OutputNode { visitor.visit_layer_surface(surface.deref()); } } + for item in self.tray_items.iter() { + visitor.visit_tray_item(&item); + } } fn node_visible(&self) -> bool { @@ -1321,6 +1365,18 @@ impl Node for OutputNode { let (x, y) = non_exclusive_rect.translate(x, y); if y < bar_height { search_layers = false; + for item in self.tray_items.iter() { + let pos = item.rel_pos.get(); + if pos.contains(x, y) { + let (x, y) = pos.translate(x, y); + tree.push(FoundNode { + node: item.deref().clone(), + x, + y, + }); + return item.node_find_tree_at(x, y, tree, usecase); + } + } } else { if let Some(ws) = self.workspace.get() { let y = y - bar_height; diff --git a/src/tree/walker.rs b/src/tree/walker.rs index 808cdd7d..f3bd3582 100644 --- a/src/tree/walker.rs +++ b/src/tree/walker.rs @@ -2,6 +2,7 @@ use { crate::{ ifs::wl_surface::{ ext_session_lock_surface_v1::ExtSessionLockSurfaceV1, + ext_tray_item_v1::ExtTrayItemV1, x_surface::xwindow::Xwindow, xdg_surface::{xdg_popup::XdgPopup, xdg_toplevel::XdgToplevel}, zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, @@ -62,6 +63,10 @@ pub trait NodeVisitorBase: Sized { fn visit_lock_surface(&mut self, node: &Rc) { node.node_visit_children(self); } + + fn visit_tray_item(&mut self, node: &Rc) { + node.node_visit_children(self); + } } pub trait NodeVisitor { @@ -77,6 +82,7 @@ pub trait NodeVisitor { fn visit_xwindow(&mut self, node: &Rc); fn visit_placeholder(&mut self, node: &Rc); fn visit_lock_surface(&mut self, node: &Rc); + fn visit_tray_item(&mut self, node: &Rc); } impl NodeVisitor for T { @@ -127,6 +133,10 @@ impl NodeVisitor for T { fn visit_lock_surface(&mut self, node: &Rc) { ::visit_lock_surface(self, node) } + + fn visit_tray_item(&mut self, node: &Rc) { + ::visit_tray_item(self, node) + } } pub struct GenericNodeVisitor { @@ -197,6 +207,11 @@ impl)> NodeVisitor for GenericNodeVisitor { (self.f)(node.clone()); node.node_visit_children(self); } + + fn visit_tray_item(&mut self, node: &Rc) { + (self.f)(node.clone()); + node.node_visit_children(self); + } } // pub fn visit_containers)>(f: F) -> impl NodeVisitor { diff --git a/src/utils/numcell.rs b/src/utils/numcell.rs index 18739943..a8fc11c1 100644 --- a/src/utils/numcell.rs +++ b/src/utils/numcell.rs @@ -49,6 +49,16 @@ impl NumCell { res } + #[inline(always)] + pub fn add_fetch(&self, n: T) -> T + where + T: Copy + Add, + { + let res = self.t.get() + n; + self.t.set(res); + res + } + #[inline(always)] pub fn fetch_sub(&self, n: T) -> T where diff --git a/wire/ext_tray_item_v1.txt b/wire/ext_tray_item_v1.txt new file mode 100644 index 00000000..dacd741d --- /dev/null +++ b/wire/ext_tray_item_v1.txt @@ -0,0 +1,30 @@ +request destroy { +} + +request ack_configure { + serial: u32, +} + +request get_popup { + popup: id(xdg_popup), + seat: id(wl_seat), + serial: u32, + keyboard_focus: u32, +} + +event configure_size { + width: i32, + height: i32, +} + +event preferred_anchor { + anchor: u32, +} + +event preferred_gravity { + gravity: u32, +} + +event configure { + serial: u32, +} diff --git a/wire/ext_tray_v1.txt b/wire/ext_tray_v1.txt new file mode 100644 index 00000000..54b5a979 --- /dev/null +++ b/wire/ext_tray_v1.txt @@ -0,0 +1,7 @@ +request destroy { +} + +request get_tray_item { + id: id(ext_tray_item_v1), + surface: id(wl_surface), +}