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/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 bd3f28e4..feab47e4 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::{cell::RefCell, future::Future, rc::Rc}; // DBus state shared between systray instances, to avoid creating too many connections etc. struct DBusSession { @@ -23,14 +25,20 @@ 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, + 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) { @@ -46,14 +54,20 @@ impl Props { } struct Tray { - menubar: gtk::MenuBar, + container: gtk::Box, items: std::collections::HashMap, icon_size: tokio::sync::watch::Receiver, + prepend_new: Rc>, } -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(), + prepend_new: props.prepend_new.clone(), + }; let task = glib::MainContext::default().spawn_local(async move { let s = match dbus_session().await { @@ -64,13 +78,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(); }); } @@ -78,15 +92,19 @@ 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); + 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.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); } @@ -96,7 +114,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>, @@ -112,7 +130,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 { @@ -125,8 +143,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 @@ -135,9 +153,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 @@ -156,9 +173,52 @@ 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 + ); + + let result = match (evt.button(), item_is_menu) { + (gdk::BUTTON_PRIMARY, 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, _) => 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 }) + } + _ => 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) + })); + // 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 { diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index 015344c4..a1d9014b 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -1138,13 +1138,19 @@ 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 + let props_clone2 = 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 +1159,10 @@ 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)?); }, + // @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); @@ -1239,16 +1247,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/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 {} 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.