From f80e093223c6487df2ae7f850738125a4ddedd47 Mon Sep 17 00:00:00 2001 From: MoetaYuko Date: Sat, 30 Dec 2023 17:31:00 +0800 Subject: [PATCH 1/4] systray: watch the proper signal for new title --- crates/eww/src/widgets/systray.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/eww/src/widgets/systray.rs b/crates/eww/src/widgets/systray.rs index bd3f28e4..df27c948 100644 --- a/crates/eww/src/widgets/systray.rs +++ b/crates/eww/src/widgets/systray.rs @@ -158,7 +158,7 @@ impl Item { // updates let mut status_updates = item.sni.receive_new_status().await?; - let mut title_updates = item.sni.receive_new_status().await?; + let mut title_updates = item.sni.receive_new_title().await?; let mut icon_updates = item.sni.receive_new_icon().await?; loop { From 361b8d1b668defbaf7189d3c7de4c1368793417b Mon Sep 17 00:00:00 2001 From: MoetaYuko Date: Wed, 27 Dec 2023 12:32:32 +0800 Subject: [PATCH 2/4] systray: handle mouse click events --- crates/eww/src/app.rs | 23 ++++++----- crates/eww/src/display_backend.rs | 24 ++++++------ crates/eww/src/widgets/mod.rs | 1 + crates/eww/src/widgets/systray.rs | 47 +++++++++++++++++++++++ crates/eww/src/widgets/window.rs | 64 +++++++++++++++++++++++++++++++ 5 files changed, 137 insertions(+), 22 deletions(-) create mode 100644 crates/eww/src/widgets/window.rs diff --git a/crates/eww/src/app.rs b/crates/eww/src/app.rs index 8b13a671..41b3364a 100644 --- a/crates/eww/src/app.rs +++ b/crates/eww/src/app.rs @@ -7,6 +7,7 @@ use crate::{ paths::EwwPaths, script_var_handler::ScriptVarHandlerHandle, state::scope_graph::{ScopeGraph, ScopeIndex}, + widgets::window::Window, window_arguments::WindowArguments, window_initiator::WindowInitiator, *, @@ -92,7 +93,7 @@ pub struct EwwWindow { pub instance_id: String, pub name: String, pub scope_index: ScopeIndex, - pub gtk_window: gtk::Window, + pub gtk_window: Window, pub destroy_event_handler_id: Option, } @@ -524,15 +525,21 @@ fn initialize_window( window_scope: ScopeIndex, ) -> Result { let monitor_geometry = monitor.geometry(); - let window = B::initialize_window(window_init, monitor_geometry) + let (actual_window_rect, x, y) = match window_init.geometry { + Some(geometry) => { + let rect = get_window_rectangle(geometry, monitor_geometry); + (Some(rect), rect.x(), rect.y()) + } + _ => (None, 0, 0), + }; + let window = B::initialize_window(window_init, monitor_geometry, x, y) .with_context(|| format!("monitor {} is unavailable", window_init.monitor.clone().unwrap()))?; window.set_title(&format!("Eww - {}", window_init.name)); window.set_position(gtk::WindowPosition::None); window.set_gravity(gdk::Gravity::Center); - if let Some(geometry) = window_init.geometry { - let actual_window_rect = get_window_rectangle(geometry, monitor_geometry); + if let Some(actual_window_rect) = actual_window_rect { window.set_size_request(actual_window_rect.width(), actual_window_rect.height()); window.set_default_size(actual_window_rect.width(), actual_window_rect.height()); } @@ -575,11 +582,7 @@ fn initialize_window( /// Apply the provided window-positioning rules to the window. #[cfg(feature = "x11")] -fn apply_window_position( - mut window_geometry: WindowGeometry, - monitor_geometry: gdk::Rectangle, - window: >k::Window, -) -> Result<()> { +fn apply_window_position(mut window_geometry: WindowGeometry, monitor_geometry: gdk::Rectangle, window: &Window) -> Result<()> { let gdk_window = window.window().context("Failed to get gdk window from gtk window")?; window_geometry.size = Coords::from_pixels(window.size()); let actual_window_rect = get_window_rectangle(window_geometry, monitor_geometry); @@ -593,7 +596,7 @@ fn apply_window_position( Ok(()) } -fn on_screen_changed(window: >k::Window, _old_screen: Option<&gdk::Screen>) { +fn on_screen_changed(window: &Window, _old_screen: Option<&gdk::Screen>) { let visual = gtk::prelude::GtkWindowExt::screen(window) .and_then(|screen| screen.rgba_visual().filter(|_| screen.is_composited()).or_else(|| screen.system_visual())); window.set_visual(visual.as_ref()); diff --git a/crates/eww/src/display_backend.rs b/crates/eww/src/display_backend.rs index 74a6033e..d416166b 100644 --- a/crates/eww/src/display_backend.rs +++ b/crates/eww/src/display_backend.rs @@ -1,4 +1,4 @@ -use crate::window_initiator::WindowInitiator; +use crate::{widgets::window::Window, window_initiator::WindowInitiator}; #[cfg(feature = "wayland")] pub use platform_wayland::WaylandBackend; @@ -9,7 +9,7 @@ pub use platform_x11::{set_xprops, X11Backend}; pub trait DisplayBackend: Send + Sync + 'static { const IS_X11: bool; - fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle) -> Option; + fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle, x: i32, y: i32) -> Option; } pub struct NoBackend; @@ -17,14 +17,14 @@ pub struct NoBackend; impl DisplayBackend for NoBackend { const IS_X11: bool = false; - fn initialize_window(_window_init: &WindowInitiator, _monitor: gdk::Rectangle) -> Option { - Some(gtk::Window::new(gtk::WindowType::Toplevel)) + fn initialize_window(_window_init: &WindowInitiator, _monitor: gdk::Rectangle, x: i32, y: i32) -> Option { + Some(Window::new(gtk::WindowType::Toplevel, x, y)) } } #[cfg(feature = "wayland")] mod platform_wayland { - use crate::window_initiator::WindowInitiator; + use crate::{widgets::window::Window, window_initiator::WindowInitiator}; use gtk::prelude::*; use yuck::config::{window_definition::WindowStacking, window_geometry::AnchorAlignment}; @@ -35,8 +35,8 @@ mod platform_wayland { impl DisplayBackend for WaylandBackend { const IS_X11: bool = false; - fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle) -> Option { - let window = gtk::Window::new(gtk::WindowType::Toplevel); + fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle, x: i32, y: i32) -> Option { + let window = Window::new(gtk::WindowType::Toplevel, x, y); // Initialising a layer shell surface gtk_layer_shell::init_for_window(&window); // Sets the monitor where the surface is shown @@ -112,7 +112,7 @@ mod platform_wayland { #[cfg(feature = "x11")] mod platform_x11 { - use crate::window_initiator::WindowInitiator; + use crate::{widgets::window::Window, window_initiator::WindowInitiator}; use anyhow::{Context, Result}; use gdk::Monitor; use gtk::{self, prelude::*}; @@ -135,10 +135,10 @@ mod platform_x11 { impl DisplayBackend for X11Backend { const IS_X11: bool = true; - fn initialize_window(window_init: &WindowInitiator, _monitor: gdk::Rectangle) -> Option { + fn initialize_window(window_init: &WindowInitiator, _monitor: gdk::Rectangle, x: i32, y: i32) -> Option { let window_type = if window_init.backend_options.x11.wm_ignore { gtk::WindowType::Popup } else { gtk::WindowType::Toplevel }; - let window = gtk::Window::new(window_type); + let window = Window::new(window_type, x, y); window.set_resizable(window_init.resizable); window.set_keep_above(window_init.stacking == WindowStacking::Foreground); window.set_keep_below(window_init.stacking == WindowStacking::Background); @@ -151,7 +151,7 @@ mod platform_x11 { } } - pub fn set_xprops(window: >k::Window, monitor: Monitor, window_init: &WindowInitiator) -> Result<()> { + pub fn set_xprops(window: &Window, monitor: Monitor, window_init: &WindowInitiator) -> Result<()> { let backend = X11BackendConnection::new()?; backend.set_xprops_for(window, monitor, window_init)?; Ok(()) @@ -171,7 +171,7 @@ mod platform_x11 { Ok(X11BackendConnection { conn, root_window: screen.root, atoms }) } - fn set_xprops_for(&self, window: >k::Window, monitor: Monitor, window_init: &WindowInitiator) -> Result<()> { + fn set_xprops_for(&self, window: &Window, monitor: Monitor, window_init: &WindowInitiator) -> Result<()> { let monitor_rect = monitor.geometry(); let scale_factor = monitor.scale_factor() as u32; let gdk_window = window.window().context("Couldn't get gdk window from gtk window")?; diff --git a/crates/eww/src/widgets/mod.rs b/crates/eww/src/widgets/mod.rs index 35d05cf2..34abab0c 100644 --- a/crates/eww/src/widgets/mod.rs +++ b/crates/eww/src/widgets/mod.rs @@ -7,6 +7,7 @@ pub mod graph; mod systray; pub mod transform; pub mod widget_definitions; +pub mod window; /// Run a command that was provided as an attribute. /// This command may use placeholders which will be replaced by the values of the arguments given. diff --git a/crates/eww/src/widgets/systray.rs b/crates/eww/src/widgets/systray.rs index df27c948..13e381ea 100644 --- a/crates/eww/src/widgets/systray.rs +++ b/crates/eww/src/widgets/systray.rs @@ -1,6 +1,8 @@ +use crate::widgets::window::Window; use futures::StreamExt; use gtk::{cairo::Surface, gdk::ffi::gdk_cairo_surface_create_from_pixbuf, prelude::*}; use notifier_host; +use std::{future::Future, rc::Rc}; // DBus state shared between systray instances, to avoid creating too many connections etc. struct DBusSession { @@ -23,6 +25,11 @@ async fn dbus_session() -> zbus::Result<&'static DBusSession> { .await } +fn run_async_task(f: F) -> F::Output { + let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().expect("Failed to initialize tokio runtime"); + rt.block_on(f) +} + pub struct Props { icon_size_tx: tokio::sync::watch::Sender, } @@ -156,6 +163,46 @@ impl Item { let scale = icon.scale_factor(); load_icon_for_item(&icon, &item, *icon_size.borrow_and_update(), scale).await; + let item = Rc::new(item); + let window = + widget.toplevel().expect("Failed to obtain toplevel window").downcast::().expect("Failed to downcast window"); + widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK); + widget.connect_button_press_event(glib::clone!(@strong item => move |_, evt| { + let (x, y) = (evt.root().0 as i32 + window.x(), evt.root().1 as i32 + window.y()); + let item_is_menu = run_async_task(async { item.sni.item_is_menu().await }); + let have_item_is_menu = item_is_menu.is_ok(); + let item_is_menu = item_is_menu.unwrap_or(false); + log::debug!( + "mouse click button={}, x={}, y={}, have_item_is_menu={}, item_is_menu={}", + evt.button(), + x, + y, + have_item_is_menu, + item_is_menu + ); + + match (evt.button(), item_is_menu) { + (gdk::BUTTON_PRIMARY, false) => { + if let Err(e) = run_async_task(async { item.sni.activate(x, y).await }) { + log::error!("failed to send activate event: {}", e); + if !have_item_is_menu { + // Some applications are in fact menu-only (don't have Activate method) + // but don't report so through ItemIsMenu property. Fallback to menu if + // activate failed in this case. + return gtk::Inhibit(false); + } + } + } + (gdk::BUTTON_MIDDLE, _) => { + if let Err(e) = run_async_task(async { item.sni.secondary_activate(x, y).await }) { + log::error!("failed to send secondary activate event: {}", e); + } + } + _ => return gtk::Inhibit(false), + } + gtk::Inhibit(true) + })); + // updates let mut status_updates = item.sni.receive_new_status().await?; let mut title_updates = item.sni.receive_new_title().await?; diff --git a/crates/eww/src/widgets/window.rs b/crates/eww/src/widgets/window.rs new file mode 100644 index 00000000..4e6c0dc2 --- /dev/null +++ b/crates/eww/src/widgets/window.rs @@ -0,0 +1,64 @@ +use glib::{object_subclass, wrapper}; +use glib_macros::Properties; +use gtk::{prelude::*, subclass::prelude::*}; +use std::cell::RefCell; + +wrapper! { + pub struct Window(ObjectSubclass) + @extends gtk::Window, gtk::Bin, gtk::Container, gtk::Widget, @implements gtk::Buildable; +} + +#[derive(Properties)] +#[properties(wrapper_type = Window)] +pub struct WindowPriv { + #[property(get, name = "x", nick = "X", blurb = "Global x coordinate", default = 0)] + x: RefCell, + + #[property(get, name = "y", nick = "Y", blurb = "Global y coordinate", default = 0)] + y: RefCell, +} + +// This should match the default values from the ParamSpecs +impl Default for WindowPriv { + fn default() -> Self { + WindowPriv { x: RefCell::new(0), y: RefCell::new(0) } + } +} + +#[object_subclass] +impl ObjectSubclass for WindowPriv { + type ParentType = gtk::Window; + type Type = Window; + + const NAME: &'static str = "WindowEww"; +} + +impl Default for Window { + fn default() -> Self { + glib::Object::new::() + } +} + +impl Window { + pub fn new(type_: gtk::WindowType, x_: i32, y_: i32) -> Self { + let w: Self = glib::Object::builder().property("type", type_).build(); + let priv_ = w.imp(); + priv_.x.replace(x_); + priv_.y.replace(y_); + w + } +} + +impl ObjectImpl for WindowPriv { + fn properties() -> &'static [glib::ParamSpec] { + Self::derived_properties() + } + + fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { + self.derived_property(id, pspec) + } +} +impl WindowImpl for WindowPriv {} +impl BinImpl for WindowPriv {} +impl ContainerImpl for WindowPriv {} +impl WidgetImpl for WindowPriv {} From e74dd6fa45f3a01d8cc70cd080ad4dd58aae37d1 Mon Sep 17 00:00:00 2001 From: MoetaYuko Date: Sat, 30 Dec 2023 21:38:55 +0800 Subject: [PATCH 3/4] systray: replace MenuBar/MenuItem with Box/EventBox The major benefit of MenuItem is automatic handling of context menus. However, MenuItem cannot properly process right mouse click, making it less useful. Hence, this patch replaces it (as long as the container) with a simple EventBox and process button clicks on our own. --- Cargo.lock | 1 + crates/eww/src/widgets/systray.rs | 60 ++++++++++---------- crates/eww/src/widgets/widget_definitions.rs | 25 +++----- crates/notifier_host/Cargo.toml | 1 + crates/notifier_host/src/item.rs | 20 +++++-- 5 files changed, 57 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ab6a187..8e3af352 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1954,6 +1954,7 @@ name = "notifier_host" version = "0.1.0" dependencies = [ "dbusmenu-gtk3", + "gdk", "gtk", "log", "thiserror", diff --git a/crates/eww/src/widgets/systray.rs b/crates/eww/src/widgets/systray.rs index 13e381ea..2a9b46f1 100644 --- a/crates/eww/src/widgets/systray.rs +++ b/crates/eww/src/widgets/systray.rs @@ -53,14 +53,14 @@ impl Props { } struct Tray { - menubar: gtk::MenuBar, + container: gtk::Box, items: std::collections::HashMap, icon_size: tokio::sync::watch::Receiver, } -pub fn spawn_systray(menubar: >k::MenuBar, props: &Props) { - let mut systray = Tray { menubar: menubar.clone(), items: Default::default(), icon_size: props.icon_size_tx.subscribe() }; +pub fn spawn_systray(container: >k::Box, props: &Props) { + let mut systray = Tray { container: container.clone(), items: Default::default(), icon_size: props.icon_size_tx.subscribe() }; let task = glib::MainContext::default().spawn_local(async move { let s = match dbus_session().await { @@ -71,13 +71,13 @@ pub fn spawn_systray(menubar: >k::MenuBar, props: &Props) { } }; - systray.menubar.show(); + systray.container.show(); let e = notifier_host::run_host(&mut systray, &s.snw).await; log::error!("notifier host error: {}", e); }); // stop the task when the widget is dropped - menubar.connect_destroy(move |_| { + container.connect_destroy(move |_| { task.abort(); }); } @@ -85,15 +85,15 @@ pub fn spawn_systray(menubar: >k::MenuBar, props: &Props) { impl notifier_host::Host for Tray { fn add_item(&mut self, id: &str, item: notifier_host::Item) { let item = Item::new(id.to_owned(), item, self.icon_size.clone()); - self.menubar.add(&item.widget); + self.container.add(&item.widget); if let Some(old_item) = self.items.insert(id.to_string(), item) { - self.menubar.remove(&old_item.widget); + self.container.remove(&old_item.widget); } } fn remove_item(&mut self, id: &str) { if let Some(item) = self.items.get(id) { - self.menubar.remove(&item.widget); + self.container.remove(&item.widget); } else { log::warn!("Tried to remove nonexistent item {:?} from systray", id); } @@ -103,7 +103,7 @@ impl notifier_host::Host for Tray { /// Item represents a single icon being shown in the system tray. struct Item { /// Main widget representing this tray item. - widget: gtk::MenuItem, + widget: gtk::EventBox, /// Async task to stop when this item gets removed. task: Option>, @@ -119,7 +119,7 @@ impl Drop for Item { impl Item { fn new(id: String, item: notifier_host::Item, icon_size: tokio::sync::watch::Receiver) -> Self { - let widget = gtk::MenuItem::new(); + let widget = gtk::EventBox::new(); let out_widget = widget.clone(); // copy so we can return it let task = glib::MainContext::default().spawn_local(async move { @@ -132,8 +132,8 @@ impl Item { } async fn maintain( - widget: gtk::MenuItem, - item: notifier_host::Item, + widget: gtk::EventBox, + mut item: notifier_host::Item, mut icon_size: tokio::sync::watch::Receiver, ) -> zbus::Result<()> { // init icon @@ -142,9 +142,8 @@ impl Item { icon.show(); // init menu - match item.menu().await { - Ok(m) => widget.set_submenu(Some(&m)), - Err(e) => log::warn!("failed to get menu: {}", e), + if let Err(e) = item.set_menu(&widget).await { + log::warn!("failed to set menu: {}", e); } // TODO this is a lot of code duplication unfortunately, i'm not really sure how to @@ -181,24 +180,27 @@ impl Item { item_is_menu ); - match (evt.button(), item_is_menu) { + let result = match (evt.button(), item_is_menu) { (gdk::BUTTON_PRIMARY, false) => { - if let Err(e) = run_async_task(async { item.sni.activate(x, y).await }) { - log::error!("failed to send activate event: {}", e); - if !have_item_is_menu { - // Some applications are in fact menu-only (don't have Activate method) - // but don't report so through ItemIsMenu property. Fallback to menu if - // activate failed in this case. - return gtk::Inhibit(false); - } + let result = run_async_task(async { item.sni.activate(x, y).await }); + if result.is_err() && !have_item_is_menu { + log::debug!("fallback to context menu due to: {}", result.unwrap_err()); + // Some applications are in fact menu-only (don't have Activate method) + // but don't report so through ItemIsMenu property. Fallback to menu if + // activate failed in this case. + run_async_task(async { item.popup_menu( evt, x, y).await }) + } else { + result } } - (gdk::BUTTON_MIDDLE, _) => { - if let Err(e) = run_async_task(async { item.sni.secondary_activate(x, y).await }) { - log::error!("failed to send secondary activate event: {}", e); - } + (gdk::BUTTON_MIDDLE, _) => run_async_task(async { item.sni.secondary_activate(x, y).await }), + (gdk::BUTTON_SECONDARY, _) | (gdk::BUTTON_PRIMARY, true) => { + run_async_task(async { item.popup_menu( evt, x, y).await }) } - _ => return gtk::Inhibit(false), + _ => Err(zbus::Error::Failure(format!("unknown button {}", evt.button()))), + }; + if let Err(result) = result { + log::error!("failed to handle mouse click {}: {}", evt.button(), result); } gtk::Inhibit(true) })); diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index 015344c4..18dff789 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -1138,13 +1138,18 @@ fn build_graph(bargs: &mut BuilderArgs) -> Result { const WIDGET_NAME_SYSTRAY: &str = "systray"; /// @widget systray /// @desc Tray for system notifier icons -fn build_systray(bargs: &mut BuilderArgs) -> Result { - let gtk_widget = gtk::MenuBar::new(); +fn build_systray(bargs: &mut BuilderArgs) -> Result { + let gtk_widget = gtk::Box::new(gtk::Orientation::Horizontal, 0); let props = Rc::new(systray::Props::new()); - let props_clone = props.clone(); + let props_clone = props.clone(); // copies for def_widget - // copies for def_widget def_widget!(bargs, _g, gtk_widget, { + // @prop spacing - spacing between elements + prop(spacing: as_i32 = 0) { gtk_widget.set_spacing(spacing) }, + // @prop orientation - orientation of the box. possible values: $orientation + prop(orientation: as_string) { gtk_widget.set_orientation(parse_orientation(&orientation)?) }, + // @prop space-evenly - space the widgets evenly. + prop(space_evenly: as_bool = true) { gtk_widget.set_homogeneous(space_evenly) }, // @prop icon-size - size of icons in the tray prop(icon_size: as_i32) { if icon_size <= 0 { @@ -1153,8 +1158,6 @@ fn build_systray(bargs: &mut BuilderArgs) -> Result { props.icon_size(icon_size); } }, - // @prop pack-direction - how to arrange tray items - prop(pack_direction: as_string) { gtk_widget.set_pack_direction(parse_packdirection(&pack_direction)?); }, }); systray::spawn_systray(>k_widget, &props_clone); @@ -1239,16 +1242,6 @@ fn parse_gravity(g: &str) -> Result { } } -/// @var pack-direction - "right", "ltr", "left", "rtl", "down", "ttb", "up", "btt" -fn parse_packdirection(o: &str) -> Result { - enum_parse! { "packdirection", o, - "right" | "ltr" => gtk::PackDirection::Ltr, - "left" | "rtl" => gtk::PackDirection::Rtl, - "down" | "ttb" => gtk::PackDirection::Ttb, - "up" | "btt" => gtk::PackDirection::Btt, - } -} - /// Connect a function to the first map event of a widget. After that first map, the handler will get disconnected. fn connect_first_map, F: Fn(&W) + 'static>(widget: &W, func: F) { let signal_handler_id = std::rc::Rc::new(std::cell::RefCell::new(None)); diff --git a/crates/notifier_host/Cargo.toml b/crates/notifier_host/Cargo.toml index a03a2a2f..10a7d6cb 100644 --- a/crates/notifier_host/Cargo.toml +++ b/crates/notifier_host/Cargo.toml @@ -10,6 +10,7 @@ homepage = "https://github.com/elkowar/eww" [dependencies] gtk = "0.17.1" +gdk = "0.17.1" zbus = { version = "3.7.0", default-features = false, features = ["tokio"] } dbusmenu-gtk3 = "0.1.0" diff --git a/crates/notifier_host/src/item.rs b/crates/notifier_host/src/item.rs index c258957e..70317ebd 100644 --- a/crates/notifier_host/src/item.rs +++ b/crates/notifier_host/src/item.rs @@ -42,6 +42,7 @@ impl std::str::FromStr for Status { pub struct Item { /// The StatusNotifierItem that is wrapped by this instance. pub sni: proxy::StatusNotifierItemProxy<'static>, + gtk_menu: Option, } impl Item { @@ -68,7 +69,7 @@ impl Item { let sni = proxy::StatusNotifierItemProxy::builder(con).destination(addr)?.path(path)?.build().await?; - Ok(Item { sni }) + Ok(Self { sni, gtk_menu: None }) } /// Get the current status of the item. @@ -80,11 +81,20 @@ impl Item { } } - /// Get the menu of this item. - pub async fn menu(&self) -> zbus::Result { - // TODO document what this returns if there is no menu. + pub async fn set_menu(&mut self, widget: >k::EventBox) -> zbus::Result<()> { let menu = dbusmenu_gtk3::Menu::new(self.sni.destination(), &self.sni.menu().await?); - Ok(menu.upcast()) + menu.set_attach_widget(Some(widget)); + self.gtk_menu = Some(menu); + Ok(()) + } + + pub async fn popup_menu(&self, event: &gdk::EventButton, x: i32, y: i32) -> zbus::Result<()> { + if let Some(menu) = &self.gtk_menu { + menu.popup_at_pointer(event.downcast_ref::()); + Ok(()) + } else { + self.sni.context_menu(x, y).await + } } /// Get the current icon. From 149727ce1f7dd4f461ab1d61d560546f3d1f32a1 Mon Sep 17 00:00:00 2001 From: MoetaYuko Date: Mon, 26 Feb 2024 13:38:35 +0800 Subject: [PATCH 4/4] systray: add prepend-new property to prepend new icons to the container --- crates/eww/src/widgets/systray.rs | 19 +++++++++++++++---- crates/eww/src/widgets/widget_definitions.rs | 5 +++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/crates/eww/src/widgets/systray.rs b/crates/eww/src/widgets/systray.rs index 2a9b46f1..feab47e4 100644 --- a/crates/eww/src/widgets/systray.rs +++ b/crates/eww/src/widgets/systray.rs @@ -2,7 +2,7 @@ use crate::widgets::window::Window; use futures::StreamExt; use gtk::{cairo::Surface, gdk::ffi::gdk_cairo_surface_create_from_pixbuf, prelude::*}; use notifier_host; -use std::{future::Future, rc::Rc}; +use std::{cell::RefCell, future::Future, rc::Rc}; // DBus state shared between systray instances, to avoid creating too many connections etc. struct DBusSession { @@ -32,12 +32,13 @@ fn run_async_task(f: F) -> F::Output { pub struct Props { icon_size_tx: tokio::sync::watch::Sender, + pub prepend_new: Rc>, } impl Props { pub fn new() -> Self { let (icon_size_tx, _) = tokio::sync::watch::channel(24); - Self { icon_size_tx } + Self { icon_size_tx, prepend_new: Rc::new(RefCell::new(false)) } } pub fn icon_size(&self, value: i32) { @@ -57,10 +58,16 @@ struct Tray { items: std::collections::HashMap, icon_size: tokio::sync::watch::Receiver, + prepend_new: Rc>, } pub fn spawn_systray(container: >k::Box, props: &Props) { - let mut systray = Tray { container: container.clone(), items: Default::default(), icon_size: props.icon_size_tx.subscribe() }; + let mut systray = Tray { + container: container.clone(), + items: Default::default(), + icon_size: props.icon_size_tx.subscribe(), + prepend_new: props.prepend_new.clone(), + }; let task = glib::MainContext::default().spawn_local(async move { let s = match dbus_session().await { @@ -85,7 +92,11 @@ pub fn spawn_systray(container: >k::Box, props: &Props) { impl notifier_host::Host for Tray { fn add_item(&mut self, id: &str, item: notifier_host::Item) { let item = Item::new(id.to_owned(), item, self.icon_size.clone()); - self.container.add(&item.widget); + if *self.prepend_new.borrow() { + self.container.pack_end(&item.widget, true, true, 0); + } else { + self.container.pack_start(&item.widget, true, true, 0); + } if let Some(old_item) = self.items.insert(id.to_string(), item) { self.container.remove(&old_item.widget); } diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index 18dff789..a1d9014b 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -1142,6 +1142,7 @@ fn build_systray(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Box::new(gtk::Orientation::Horizontal, 0); let props = Rc::new(systray::Props::new()); let props_clone = props.clone(); // copies for def_widget + let props_clone2 = props.clone(); // copies for def_widget def_widget!(bargs, _g, gtk_widget, { // @prop spacing - spacing between elements @@ -1158,6 +1159,10 @@ fn build_systray(bargs: &mut BuilderArgs) -> Result { props.icon_size(icon_size); } }, + // @prop prepend-new - prepend new icons. + prop(prepend_new: as_bool = true) { + *props_clone2.prepend_new.borrow_mut() = prepend_new; + }, }); systray::spawn_systray(>k_widget, &props_clone);