diff --git a/crates/eww/src/app.rs b/crates/eww/src/app.rs index 8b13a67..41b3364 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 74a6033..d416166 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 35d05cf..34abab0 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 df27c94..13e381e 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 0000000..4e6c0dc --- /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 {}