diff --git a/docs/config.md b/docs/config.md index f4afe007..6e53c9c1 100644 --- a/docs/config.md +++ b/docs/config.md @@ -32,7 +32,10 @@ keymap = """ """ # An action that will be executed when the GPU has been initialized. -on-graphics-initialized = { type = "exec", exec = "mako" } +on-graphics-initialized = [ + { type = "exec", exec = "mako" }, + { type = "exec", exec = "wl-tray-bridge" }, +] # Shortcuts that are processed by the compositor. # The left hand side should be a key, possibly prefixed with modifiers. @@ -266,7 +269,10 @@ If you want to run an action at startup, you can use the top-level `on-graphics- field: ```toml -on-graphics-initialized = { type = "exec", exec = "mako" } +on-graphics-initialized = [ + { type = "exec", exec = "mako" }, + { type = "exec", exec = "wl-tray-bridge" }, +] ``` ### Setting Environment Variables @@ -490,7 +496,7 @@ output.name = "left" See the specification for more details. -# Theming +### Theming You can configure the colors, sizes, and fonts used by the compositor with the top-level `theme` table. @@ -500,3 +506,10 @@ bg-color = "#ff000" ``` See the specification for more details. + +### Tray Icons and Menus + +The default configuration will try to start [wl-tray-bridge] to give you access to tray +icons and menus. + +[wl-tray-bridge]: https://github.com/mahkoh/wl-tray-bridge diff --git a/docs/features.md b/docs/features.md index 053f70a8..fbb83d60 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 | +| jay_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..4d2e8679 100644 --- a/release-notes.md +++ b/release-notes.md @@ -11,6 +11,8 @@ - Fix screen sharing in zoom. - Implement wp-fifo-v1. - Implement wp-commit-timing-v1. +- Implement jay-tray-v1. You can get tray icons and menus by using + https://github.com/mahkoh/wl-tray-bridge. # 1.6.0 (2024-09-25) diff --git a/src/client.rs b/src/client.rs index 70a7f586..71fdeb5c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -175,6 +175,7 @@ impl Clients { slf, )), wire_scale: Default::default(), + focus_stealing_serial: Default::default(), }); track!(data, data); let display = Rc::new(WlDisplay::new(&data)); @@ -286,6 +287,7 @@ pub struct Client { pub activation_tokens: RefCell>, pub commit_timelines: Rc, pub wire_scale: Cell>, + pub focus_stealing_serial: Cell>, } pub const NUM_CACHED_SERIAL_RANGES: usize = 64; diff --git a/src/compositor.rs b/src/compositor.rs index 61236341..a466757b 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -273,6 +273,7 @@ fn start_compositor2( ui_drag_threshold_squared: Cell::new(10), toplevels: Default::default(), const_40hz_latch: Default::default(), + tray_item_ids: Default::default(), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); @@ -586,6 +587,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..2b83a7fc 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -26,6 +26,7 @@ pub mod jay_seat_events; pub mod jay_select_toplevel; pub mod jay_select_workspace; pub mod jay_toplevel; +pub mod jay_tray_v1; pub mod jay_workspace; pub mod jay_workspace_watcher; pub mod jay_xwayland; diff --git a/src/ifs/jay_tray_v1.rs b/src/ifs/jay_tray_v1.rs new file mode 100644 index 00000000..cc36d3ec --- /dev/null +++ b/src/ifs/jay_tray_v1.rs @@ -0,0 +1,109 @@ +use { + crate::{ + client::{Client, ClientError}, + globals::{Global, GlobalName, RemovableWaylandGlobal}, + ifs::{ + wl_output::OutputGlobalOpt, + wl_surface::tray::jay_tray_item_v1::{JayTrayItemV1, JayTrayItemV1Error}, + }, + leaks::Tracker, + object::{Object, Version}, + wire::{jay_tray_v1::*, JayTrayV1Id}, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct JayTrayV1Global { + pub name: GlobalName, + pub output: Rc, +} + +pub struct JayTrayV1 { + pub id: JayTrayV1Id, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, + pub output: Rc, +} + +impl JayTrayV1Global { + fn bind_( + self: Rc, + id: JayTrayV1Id, + client: &Rc, + version: Version, + ) -> Result<(), JayTrayManagerV1Error> { + let obj = Rc::new(JayTrayV1 { + id, + client: client.clone(), + tracker: Default::default(), + version, + output: self.output.clone(), + }); + track!(client, obj); + client.add_client_obj(&obj)?; + Ok(()) + } +} + +global_base!(JayTrayV1Global, JayTrayV1, JayTrayManagerV1Error); + +impl Global for JayTrayV1Global { + fn singleton(&self) -> bool { + false + } + + fn version(&self) -> u32 { + 1 + } +} + +simple_add_global!(JayTrayV1Global); + +impl RemovableWaylandGlobal for JayTrayV1Global { + fn create_replacement(self: Rc) -> Rc { + self + } +} + +impl JayTrayV1RequestHandler for JayTrayV1 { + type Error = JayTrayManagerV1Error; + + 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(JayTrayItemV1::new( + req.id, + self.version, + &surface, + &self.output, + )); + track!(self.client, fs); + fs.install()?; + self.client.add_client_obj(&fs)?; + Ok(()) + } +} + +object_base! { + self = JayTrayV1; + version = self.version; +} + +impl Object for JayTrayV1 {} + +simple_add_obj!(JayTrayV1); + +#[derive(Debug, Error)] +pub enum JayTrayManagerV1Error { + #[error(transparent)] + ClientError(Box), + #[error(transparent)] + ExtTrayItemV1Error(#[from] JayTrayItemV1Error), +} +efrom!(JayTrayManagerV1Error, 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 df190682..86b5bd2c 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,12 @@ use { zwp_pointer_gesture_swipe_v1::ZwpPointerGestureSwipeV1, zwp_relative_pointer_v1::ZwpRelativePointerV1, }, - wl_surface::{dnd_icon::DndIcon, WlSurface}, + wl_surface::{ + dnd_icon::DndIcon, + tray::{DynTrayItem, TrayItemId}, + xdg_surface::xdg_popup::XdgPopup, + WlSurface, + }, xdg_toplevel_drag_v1::XdgToplevelDragV1, }, leaks::Tracker, @@ -82,8 +88,8 @@ use { }, wire::{ wl_seat::*, ExtIdleNotificationV1Id, WlDataDeviceId, WlKeyboardId, WlPointerId, - WlSeatId, WlTouchId, ZwlrDataControlDeviceV1Id, ZwpPrimarySelectionDeviceV1Id, - ZwpRelativePointerV1Id, ZwpTextInputV3Id, + WlSeatId, WlTouchId, XdgPopupId, ZwlrDataControlDeviceV1Id, + ZwpPrimarySelectionDeviceV1Id, ZwpRelativePointerV1Id, ZwpTextInputV3Id, }, wire_ei::EiSeatId, xkbcommon::{DynKeyboardState, KeyboardState, KeymapId, XkbKeymap, XkbState}, @@ -200,6 +206,8 @@ pub struct WlSeatGlobal { tablet: TabletSeatData, ei_seats: CopyHashMap<(ClientId, EiSeatId), Rc>, ui_drag_highlight: Cell>, + keyboard_node_serial: Cell, + tray_popups: CopyHashMap<(TrayItemId, XdgPopupId), Rc>, } const CHANGE_CURSOR_MOVED: u32 = 1 << 0; @@ -229,6 +237,7 @@ impl WlSeatGlobal { pointer_stack_modified: Cell::new(false), found_tree: RefCell::new(vec![]), keyboard_node: CloneCell::new(state.root.clone()), + keyboard_node_serial: Default::default(), bindings: Default::default(), x_data_devices: Default::default(), data_devices: RefCell::new(Default::default()), @@ -271,6 +280,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(); @@ -1039,6 +1049,48 @@ impl WlSeatGlobal { ei_seat.regions_changed(); }); } + + pub fn add_tray_item_popup(&self, item: &Rc, popup: &Rc) { + self.tray_popups + .set((item.data().tray_item_id, popup.id), item.clone()); + } + + pub fn remove_tray_item_popup(&self, item: &T, popup: &Rc) { + self.tray_popups + .remove(&(item.data().tray_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(); + self.tray_popups.lock().retain(|&(tray_item_id, _), item| { + let retain = Some(tray_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; + }; + let serial = serial.min(max_serial); + if serial <= self.keyboard_node_serial.get() { + return; + } + self.focus_node_with_serial(node, serial); + } } impl CursorUserOwner for WlSeatGlobal { diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index cdc1ceb7..0a8ded1d 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -159,7 +159,8 @@ impl NodeSeatState { fn release_kb_focus2(&self, focus_last: bool) { self.release_kb_grab(); while let Some((_, seat)) = self.kb_foci.pop() { - seat.kb_owner.set_kb_node(&seat, seat.state.root.clone()); + seat.kb_owner + .set_kb_node(&seat, seat.state.root.clone(), seat.state.next_serial(None)); // log::info!("keyboard_node = root"); if focus_last { seat.get_output() @@ -935,7 +936,15 @@ impl WlSeatGlobal { } pub fn focus_node(self: &Rc, node: Rc) { - self.kb_owner.set_kb_node(self, node); + if self.keyboard_node.get().node_id() == node.node_id() { + return; + } + let serial = self.state.next_serial(node.node_client().as_deref()); + self.focus_node_with_serial(node, serial); + } + + pub fn focus_node_with_serial(self: &Rc, node: Rc, serial: u64) { + self.kb_owner.set_kb_node(self, node, serial); } pub(super) fn for_each_seat(&self, ver: Version, client: ClientId, mut f: C) @@ -1141,7 +1150,10 @@ impl WlSeatGlobal { ) { let (state, pressed) = match state { KeyState::Released => (wl_pointer::RELEASED, false), - KeyState::Pressed => (wl_pointer::PRESSED, true), + KeyState::Pressed => { + surface.client.focus_stealing_serial.set(Some(serial)); + (wl_pointer::PRESSED, true) + } }; let time = (time_usec / 1000) as u32; self.surface_pointer_event(Version::ALL, surface, |p| { @@ -1150,7 +1162,7 @@ impl WlSeatGlobal { self.surface_pointer_frame(surface); if pressed { if let Some(node) = surface.get_focus_node(self.id) { - self.focus_node(node); + self.focus_node_with_serial(node, serial); } } } @@ -1363,12 +1375,13 @@ impl WlSeatGlobal { y: Fixed, ) { let serial = surface.client.next_serial(); + surface.client.focus_stealing_serial.set(Some(serial)); let time = (time_usec / 1000) as _; self.surface_touch_event(Version::ALL, surface, |t| { t.send_down(serial, time, surface.id, id, x, y) }); if let Some(node) = surface.get_focus_node(self.id) { - self.focus_node(node); + self.focus_node_with_serial(node, serial); } } diff --git a/src/ifs/wl_seat/kb_owner.rs b/src/ifs/wl_seat/kb_owner.rs index 796aeb57..caf7048b 100644 --- a/src/ifs/wl_seat/kb_owner.rs +++ b/src/ifs/wl_seat/kb_owner.rs @@ -29,8 +29,8 @@ impl KbOwnerHolder { self.owner.get().ungrab(seat) } - pub fn set_kb_node(&self, seat: &Rc, node: Rc) { - self.owner.get().set_kb_node(seat, node); + pub fn set_kb_node(&self, seat: &Rc, node: Rc, serial: u64) { + self.owner.get().set_kb_node(seat, node, serial); } pub fn clear(&self) { @@ -45,12 +45,13 @@ struct GrabKbOwner; trait KbOwner { fn grab(&self, seat: &Rc, node: Rc) -> bool; fn ungrab(&self, seat: &Rc); - fn set_kb_node(&self, seat: &Rc, node: Rc); + fn set_kb_node(&self, seat: &Rc, node: Rc, serial: u64); } impl KbOwner for DefaultKbOwner { fn grab(&self, seat: &Rc, node: Rc) -> bool { - self.set_kb_node(seat, node); + let serial = seat.state.next_serial(node.node_client().as_deref()); + self.set_kb_node(seat, node, serial); seat.kb_owner.owner.set(Rc::new(GrabKbOwner)); true } @@ -59,7 +60,7 @@ impl KbOwner for DefaultKbOwner { // nothing } - fn set_kb_node(&self, seat: &Rc, node: Rc) { + fn set_kb_node(&self, seat: &Rc, node: Rc, serial: u64) { let old = seat.keyboard_node.get(); if old.node_id() == node.node_id() { return; @@ -78,6 +79,7 @@ impl KbOwner for DefaultKbOwner { } // log::info!("focus {}", node.node_id()); node.clone().node_on_focus(seat); + seat.keyboard_node_serial.set(serial); seat.keyboard_node.set(node.clone()); seat.tablet_on_keyboard_node_change(); } @@ -92,7 +94,7 @@ impl KbOwner for GrabKbOwner { seat.kb_owner.owner.set(seat.kb_owner.default.clone()); } - fn set_kb_node(&self, _seat: &Rc, _node: Rc) { + fn set_kb_node(&self, _seat: &Rc, _node: Rc, _serial: u64) { // nothing } } 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_seat/tablet/tool.rs b/src/ifs/wl_seat/tablet/tool.rs index 61fa6306..fcda9cbf 100644 --- a/src/ifs/wl_seat/tablet/tool.rs +++ b/src/ifs/wl_seat/tablet/tool.rs @@ -211,8 +211,9 @@ impl TabletTool { tool.send_frame(time); }); if state == ToolButtonState::Pressed { + n.client.focus_stealing_serial.set(Some(serial.get())); if let Some(node) = n.get_focus_node(self.tablet.seat.id) { - self.tablet.seat.focus_node(node); + self.tablet.seat.focus_node_with_serial(node, serial.get()); } } } @@ -259,8 +260,9 @@ impl TabletTool { }); if let Some(changes) = changes { if changes.down == Some(true) { + n.client.focus_stealing_serial.set(Some(serial.get())); if let Some(node) = n.get_focus_node(self.tablet.seat.id) { - self.tablet.seat.focus_node(node); + self.tablet.seat.focus_node_with_serial(node, serial.get()); } } } diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index e19d5735..c5495b2b 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 tray; 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, + tray::TrayItemId, 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_session_lock_surface_v1.rs b/src/ifs/wl_surface/ext_session_lock_surface_v1.rs index 78a39db3..ff429495 100644 --- a/src/ifs/wl_surface/ext_session_lock_surface_v1.rs +++ b/src/ifs/wl_surface/ext_session_lock_surface_v1.rs @@ -134,7 +134,7 @@ impl Node for ExtSessionLockSurfaceV1 { } fn node_on_pointer_enter(self: Rc, seat: &Rc, _x: Fixed, _y: Fixed) { - seat.focus_node(self.surface.clone()); + seat.focus_node_with_serial(self.surface.clone(), self.client.next_serial()); } } diff --git a/src/ifs/wl_surface/tray.rs b/src/ifs/wl_surface/tray.rs new file mode 100644 index 00000000..7c8f873e --- /dev/null +++ b/src/ifs/wl_surface/tray.rs @@ -0,0 +1,406 @@ +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, + }, + }, + rect::Rect, + tree::{ + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, OutputNode, + StackedNode, + }, + utils::{ + copyhashmap::CopyHashMap, + hash_map_ext::HashMapExt, + linkedlist::{LinkedList, LinkedNode}, + numcell::NumCell, + }, + wire::{WlSeatId, XdgPopupId}, + }, + std::{ + cell::{Cell, RefCell}, + rc::Rc, + }, + thiserror::Error, +}; + +pub mod jay_tray_item_v1; + +tree_id!(TrayItemNodeId); +linear_ids!(TrayItemIds, TrayItemId, u64); + +pub struct TrayItemData { + node_id: TrayItemNodeId, + pub tray_item_id: TrayItemId, + seat_state: NodeSeatState, + client: Rc, + visible: Cell, + pub surface: Rc, + output: Rc, + attached: Cell, + sent_serial: NumCell, + ack_serial: NumCell, + linked_node: Cell>>>, + abs_pos: Cell, + pub rel_pos: Cell, +} + +impl TrayItemData { + fn new(surface: &Rc, output: &Rc) -> Self { + TrayItemData { + node_id: surface.client.state.node_ids.next(), + tray_item_id: surface.client.state.tray_item_ids.next(), + seat_state: Default::default(), + client: surface.client.clone(), + visible: Cell::new(surface.client.state.root_visible()), + surface: surface.clone(), + output: output.clone(), + attached: Default::default(), + sent_serial: Default::default(), + ack_serial: Default::default(), + linked_node: Default::default(), + abs_pos: Default::default(), + rel_pos: Default::default(), + } + } + + pub fn find_tree_at(&self, x: i32, y: i32, tree: &mut Vec) -> FindTreeResult { + self.surface.find_tree_at_(x, y, tree) + } +} + +pub trait DynTrayItem: Node { + fn send_current_configure(&self); + fn data(&self) -> &TrayItemData; + fn into_node(self: Rc) -> Rc; + fn set_position(&self, abs_pos: Rect, rel_pos: Rect); + fn destroy_popups(&self); + fn destroy_node(&self); + fn set_visible(&self, visible: bool); +} + +impl DynTrayItem for T { + fn send_current_configure(&self) { + ::send_current_configure(self) + } + + fn data(&self) -> &TrayItemData { + ::data(self) + } + + fn into_node(self: Rc) -> Rc { + self + } + + fn set_position(&self, abs_pos: Rect, rel_pos: Rect) { + let data = self.data(); + data.surface + .set_absolute_position(abs_pos.x1(), abs_pos.y1()); + data.rel_pos.set(rel_pos); + if data.abs_pos.replace(abs_pos) != abs_pos { + for popup in self.popups().lock().values() { + popup.popup.update_absolute_position(); + } + } + } + + fn destroy_popups(&self) { + for popup in self.popups().lock().drain_values() { + popup.popup.destroy_node(); + } + } + + fn destroy_node(&self) { + let data = self.data(); + data.linked_node.take(); + data.attached.set(false); + self.destroy_popups(); + data.surface.destroy_node(); + data.seat_state.destroy_node(self); + data.client.state.tree_changed(); + if let Some(node) = data.output.node() { + node.update_tray_positions(); + } + } + + fn set_visible(&self, visible: bool) { + let data = self.data(); + data.visible.set(visible); + let visible = visible && data.surface.buffer.is_some(); + data.surface.set_visible(visible); + if !visible { + self.destroy_popups(); + } + } +} + +trait TrayItem: Sized + 'static { + fn send_initial_configure(&self); + fn send_current_configure(&self); + fn data(&self) -> &TrayItemData; + fn popups(&self) -> &CopyHashMap>>; + fn visit(self: Rc, visitor: &mut dyn NodeVisitor); +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum FocusHint { + None, + OnDemand, + Immediate, +} + +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.data().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.data().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() { + let data = self.parent.data(); + if data.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( + &data.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.data().tray_item_id) + } + + fn allow_popup_focus(&self) -> bool { + match self.focus { + FocusHint::None => false, + FocusHint::OnDemand => true, + FocusHint::Immediate => true, + } + } +} + +impl SurfaceExt for T { + fn before_apply_commit( + self: Rc, + pending: &mut PendingState, + ) -> Result<(), WlSurfaceError> { + if let Some(serial) = pending.tray_item_ack_serial.take() { + self.data().ack_serial.set(serial); + } + Ok(()) + } + + fn after_apply_commit(self: Rc) { + let data = self.data(); + if data.surface.visible.get() { + if data.surface.buffer.is_none() { + self.destroy_node(); + } + } else { + if data.ack_serial.get() != data.sent_serial.get() { + return; + } + if data.surface.buffer.is_some() { + data.surface.set_visible(data.visible.get()); + if let Some(node) = data.output.node() { + if !data.attached.replace(true) { + let link = node.tray_items.add_last(self.clone()); + data.linked_node.set(Some(link)); + node.update_tray_positions(); + } + } + } + } + } + + fn extents_changed(&self) { + let data = self.data(); + if data.surface.visible.get() { + data.client.state.tree_changed(); + } + } + + fn tray_item(self: Rc) -> Option { + Some(self.data().tray_item_id) + } +} + +impl Node for T { + fn node_id(&self) -> NodeId { + self.data().node_id.into() + } + + fn node_seat_state(&self) -> &NodeSeatState { + &self.data().seat_state + } + + fn node_visit(self: Rc, visitor: &mut dyn NodeVisitor) { + self.visit(visitor); + } + + fn node_visit_children(&self, visitor: &mut dyn NodeVisitor) { + self.data().surface.clone().node_visit(visitor); + } + + fn node_visible(&self) -> bool { + self.data().surface.visible.get() + } + + fn node_absolute_position(&self) -> Rect { + self.data().surface.node_absolute_position() + } + + fn node_find_tree_at( + &self, + x: i32, + y: i32, + tree: &mut Vec, + _usecase: FindTreeUsecase, + ) -> FindTreeResult { + self.data().find_tree_at(x, y, tree) + } + + fn node_client(&self) -> Option> { + Some(self.data().client.clone()) + } + + fn node_client_id(&self) -> Option { + Some(self.data().client.id) + } +} + +fn install(item: &Rc) -> Result<(), TrayItemError> { + let data = item.data(); + data.surface.set_role(SurfaceRole::TrayItem)?; + if data.surface.ext.get().is_some() { + return Err(TrayItemError::Exists); + } + data.surface.ext.set(item.clone()); + data.surface.set_visible(false); + if let Some(node) = data.output.node() { + data.surface.set_output(&node); + item.send_initial_configure(); + } + Ok(()) +} + +fn destroy(item: &T) -> Result<(), TrayItemError> { + if item.popups().is_not_empty() { + return Err(TrayItemError::HasPopups); + } + item.destroy_node(); + item.data().surface.unset_ext(); + item.data().surface.set_visible(false); + Ok(()) +} + +fn ack_configure(item: &T, serial: u32) { + item.data() + .surface + .pending + .borrow_mut() + .tray_item_ack_serial = Some(serial); +} + +fn get_popup( + item: &Rc, + popup: XdgPopupId, + seat: WlSeatId, + serial: u32, + focus: FocusHint, +) -> Result<(), TrayItemError> { + let data = item.data(); + let popup = data.client.lookup(popup)?; + let seat = data.client.lookup(seat)?; + let seat = &seat.global; + let Some(serial) = data.client.map_serial(serial) else { + return Err(TrayItemError::InvalidSerial); + }; + if popup.parent.is_some() { + return Err(TrayItemError::PopupHasParent); + } + let Some(node) = data.output.node() else { + popup.destroy_node(); + return Ok(()); + }; + seat.add_tray_item_popup(item, &popup); + let stack = data.client.state.root.stacked.clone(); + popup.xdg.set_popup_stack(&stack); + popup.xdg.set_output(&node); + let user = Rc::new(Popup { + parent: item.clone(), + popup: popup.clone(), + seat: seat.clone(), + serial, + focus, + stack, + stack_link: Default::default(), + }); + popup.parent.set(Some(user.clone())); + item.popups().set(popup.id, user); + Ok(()) +} + +#[derive(Debug, Error)] +pub enum TrayItemError { + #[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, +} +efrom!(TrayItemError, ClientError); diff --git a/src/ifs/wl_surface/tray/jay_tray_item_v1.rs b/src/ifs/wl_surface/tray/jay_tray_item_v1.rs new file mode 100644 index 00000000..64d387b4 --- /dev/null +++ b/src/ifs/wl_surface/tray/jay_tray_item_v1.rs @@ -0,0 +1,153 @@ +use { + crate::{ + ifs::{ + wl_output::OutputGlobalOpt, + wl_surface::{ + tray::{ + ack_configure, destroy, get_popup, install, DynTrayItem, FocusHint, Popup, + TrayItem, TrayItemData, TrayItemError, + }, + WlSurface, + }, + xdg_positioner::{ANCHOR_BOTTOM_LEFT, ANCHOR_BOTTOM_RIGHT}, + }, + leaks::Tracker, + object::{Object, Version}, + tree::NodeVisitor, + utils::copyhashmap::CopyHashMap, + wire::{jay_tray_item_v1::*, JayTrayItemV1Id, XdgPopupId}, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct JayTrayItemV1 { + id: JayTrayItemV1Id, + pub tracker: Tracker, + version: Version, + data: TrayItemData, + popups: CopyHashMap>>, +} + +impl JayTrayItemV1 { + pub fn new( + id: JayTrayItemV1Id, + version: Version, + surface: &Rc, + output: &Rc, + ) -> Self { + Self { + id, + tracker: Default::default(), + version, + popups: Default::default(), + data: TrayItemData::new(surface, output), + } + } + + pub fn install(self: &Rc) -> Result<(), JayTrayItemV1Error> { + install(self)?; + Ok(()) + } + + fn send_configure_size(&self, width: i32, height: i32) { + self.data.client.event(ConfigureSize { + self_id: self.id, + width, + height, + }); + } + + fn send_preferred_anchor(&self) { + self.data.client.event(PreferredAnchor { + self_id: self.id, + anchor: ANCHOR_BOTTOM_LEFT, + }); + } + + fn send_preferred_gravity(&self) { + self.data.client.event(PreferredGravity { + self_id: self.id, + gravity: ANCHOR_BOTTOM_RIGHT, + }); + } + + fn send_configure(&self) { + self.data.client.event(Configure { + self_id: self.id, + serial: self.data.sent_serial.add_fetch(1), + }); + } +} + +impl JayTrayItemV1RequestHandler for JayTrayItemV1 { + type Error = JayTrayItemV1Error; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + destroy(self)?; + Ok(()) + } + + fn ack_configure(&self, req: AckConfigure, _slf: &Rc) -> Result<(), Self::Error> { + ack_configure(self, req.serial); + Ok(()) + } + + fn get_popup(&self, req: GetPopup, slf: &Rc) -> Result<(), Self::Error> { + let focus = match req.keyboard_focus { + 0 => FocusHint::None, + 1 => FocusHint::OnDemand, + 2 => FocusHint::Immediate, + n => return Err(JayTrayItemV1Error::InvalidFocusHint(n)), + }; + get_popup(slf, req.popup, req.seat, req.serial, focus)?; + Ok(()) + } +} + +impl TrayItem for JayTrayItemV1 { + fn send_initial_configure(&self) { + self.send_preferred_anchor(); + self.send_preferred_gravity(); + ::send_current_configure(self); + } + + fn send_current_configure(&self) { + let size = self.data.client.state.tray_icon_size().max(1); + self.send_configure_size(size, size); + self.send_configure(); + } + + fn data(&self) -> &TrayItemData { + &self.data + } + + fn popups(&self) -> &CopyHashMap>> { + &self.popups + } + + fn visit(self: Rc, visitor: &mut dyn NodeVisitor) { + visitor.visit_tray_item(&self); + } +} + +object_base! { + self = JayTrayItemV1; + version = self.version; +} + +impl Object for JayTrayItemV1 { + fn break_loops(&self) { + self.destroy_node(); + } +} + +simple_add_obj!(JayTrayItemV1); + +#[derive(Debug, Error)] +pub enum JayTrayItemV1Error { + #[error(transparent)] + TrayItemError(#[from] TrayItemError), + #[error("The focus hint {} is invalid", .0)] + InvalidFocusHint(u32), +} diff --git a/src/ifs/wl_surface/xdg_surface.rs b/src/ifs/wl_surface/xdg_surface.rs index 6acd8f00..68aacaac 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::{ + tray::TrayItemId, 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..ae2d80f5 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::{ + tray::TrayItemId, + 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..a7eb723b 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -148,6 +148,14 @@ impl Renderer<'_> { ); } } + for item in output.tray_items.iter() { + let data = item.data(); + if data.surface.buffer.is_some() { + let rect = data.rel_pos.get().move_(x, y); + let bounds = self.base.scale_rect(rect); + self.render_surface(&data.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..142a9531 100644 --- a/src/state.rs +++ b/src/state.rs @@ -47,6 +47,7 @@ use { SeatIds, WlSeatGlobal, }, wl_surface::{ + tray::TrayItemIds, wl_subsurface::SubsurfaceIds, zwp_idle_inhibitor_v1::{IdleInhibitorId, IdleInhibitorIds, ZwpIdleInhibitorV1}, zwp_input_popup_surface_v2::ZwpInputPopupSurfaceV2, @@ -222,6 +223,7 @@ pub struct State { pub ui_drag_threshold_squared: Cell, pub toplevels: CopyHashMap>, pub const_40hz_latch: EventSource, + pub tray_item_ids: TrayItemIds, } // impl Drop for State { @@ -586,7 +588,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 +1259,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..9348de66 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::{ + jay_tray_v1::JayTrayV1Global, + 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(JayTrayV1Global { + 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..482963a4 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::{tray::TrayItemId, 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..caf5fe52 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -19,6 +19,7 @@ use { }, wl_surface::{ ext_session_lock_surface_v1::ExtSessionLockSurfaceV1, + tray::DynTrayItem, 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.data().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() { + item.deref().clone().node_visit(visitor); + } } fn node_visible(&self) -> bool { @@ -1321,6 +1365,19 @@ 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 data = item.data(); + let pos = data.rel_pos.get(); + if pos.contains(x, y) { + let (x, y) = pos.translate(x, y); + tree.push(FoundNode { + node: item.deref().clone().into_node(), + x, + y, + }); + return data.find_tree_at(x, y, tree); + } + } } 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..a716f6d0 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, + tray::jay_tray_item_v1::JayTrayItemV1, 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/toml-config/src/default-config.toml b/toml-config/src/default-config.toml index 1b65db26..fd572891 100644 --- a/toml-config/src/default-config.toml +++ b/toml-config/src/default-config.toml @@ -7,7 +7,10 @@ keymap = """ }; """ -on-graphics-initialized = { type = "exec", exec = "mako" } +on-graphics-initialized = [ + { type = "exec", exec = "mako" }, + { type = "exec", exec = "wl-tray-bridge" }, +] [shortcuts] alt-h = "focus-left" diff --git a/wire/jay_tray_item_v1.txt b/wire/jay_tray_item_v1.txt new file mode 100644 index 00000000..dacd741d --- /dev/null +++ b/wire/jay_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/jay_tray_v1.txt b/wire/jay_tray_v1.txt new file mode 100644 index 00000000..dda57f15 --- /dev/null +++ b/wire/jay_tray_v1.txt @@ -0,0 +1,7 @@ +request destroy { +} + +request get_tray_item { + id: id(jay_tray_item_v1), + surface: id(wl_surface), +}