From 324f00cdf9200e3e3ecedfa68ab4c99b170242e2 Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Tue, 4 Oct 2022 23:26:07 +0100 Subject: [PATCH] feat: wlroots-agnostic support for `focused` module --- Cargo.lock | 110 ++++++++++++++++++++- Cargo.toml | 5 +- src/main.rs | 12 ++- src/modules/focused.rs | 61 +++++------- src/wayland.rs | 49 ---------- src/wayland/client.rs | 108 +++++++++++++++++++++ src/wayland/mod.rs | 54 +++++++++++ src/wayland/toplevel.rs | 126 ++++++++++++++++++++++++ src/wayland/toplevel_manager.rs | 166 ++++++++++++++++++++++++++++++++ 9 files changed, 597 insertions(+), 94 deletions(-) delete mode 100644 src/wayland.rs create mode 100644 src/wayland/client.rs create mode 100644 src/wayland/mod.rs create mode 100644 src/wayland/toplevel.rs create mode 100644 src/wayland/toplevel_manager.rs diff --git a/Cargo.lock b/Cargo.lock index 33ca2ed..8e0a49b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,6 +280,19 @@ dependencies = [ "system-deps", ] +[[package]] +name = "calloop" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22a6a8f622f797120d452c630b0ab12e1331a1a753e2039ce7868d4ac77b4ee" +dependencies = [ + "log", + "nix 0.24.2", + "slotmap", + "thiserror", + "vec_map", +] + [[package]] name = "cc" version = "1.0.73" @@ -594,6 +607,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "dlib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading", +] + [[package]] name = "downcast-rs" version = "1.2.0" @@ -1128,6 +1150,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "smithay-client-toolkit", "stray", "strip-ansi-escapes", "swayipc-async", @@ -1181,6 +1204,16 @@ version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +[[package]] +name = "libloading" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "lock_api" version = "0.4.7" @@ -1215,6 +1248,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95af15f345b17af2efc8ead6080fb8bc376f8cec1b35277b935637595fe77498" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.6.5" @@ -1740,6 +1782,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + [[package]] name = "scopeguard" version = "1.1.0" @@ -1863,12 +1911,40 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +[[package]] +name = "smithay-client-toolkit" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f307c47d32d2715eb2e0ece5589057820e0e5e70d07c247d1063e844e107f454" +dependencies = [ + "bitflags", + "calloop", + "dlib", + "lazy_static", + "log", + "memmap2", + "nix 0.24.2", + "pkg-config", + "wayland-client", + "wayland-cursor", + "wayland-protocols", +] + [[package]] name = "socket2" version = "0.4.4" @@ -2061,9 +2137,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.21.0" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89797afd69d206ccd11fb0ea560a44bbb87731d020670e79416d442919257d42" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg", "bytes", @@ -2071,7 +2147,6 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", "pin-project-lite", "socket2", "tokio-macros", @@ -2238,6 +2313,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version-compare" version = "0.1.0" @@ -2310,6 +2391,7 @@ dependencies = [ "downcast-rs", "libc", "nix 0.24.2", + "scoped-tls", "wayland-commons", "wayland-scanner", "wayland-sys", @@ -2327,6 +2409,17 @@ dependencies = [ "wayland-sys", ] +[[package]] +name = "wayland-cursor" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" +dependencies = [ + "nix 0.24.2", + "wayland-client", + "xcursor", +] + [[package]] name = "wayland-protocols" version = "0.29.5" @@ -2356,6 +2449,8 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" dependencies = [ + "dlib", + "lazy_static", "pkg-config", ] @@ -2442,6 +2537,15 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "xcursor" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" +dependencies = [ + "nom", +] + [[package]] name = "xml-rs" version = "0.8.4" diff --git a/Cargo.toml b/Cargo.toml index 9d7d189..dd909f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ derive_builder = "0.11.2" gtk = "0.15.5" gtk-layer-shell = "0.4.1" glib = "0.15.12" -tokio = { version = "1.21.0", features = ["macros", "rt-multi-thread", "time"] } +tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time"] } tracing = "0.1.36" tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } tracing-error = "0.2.0" @@ -37,4 +37,5 @@ mpd_client = "1.0.0" swayipc-async = { git = "https://github.com/JakeStanger/swayipc-rs.git", branch = "feat/derive-clone" } sysinfo = "0.26.2" wayland-client = "0.29.5" -wayland-protocols = { version = "0.29.5", features=["unstable_protocols", "client"] } \ No newline at end of file +wayland-protocols = { version = "0.29.5", features=["unstable_protocols", "client"] } +smithay-client-toolkit = "0.16.0" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index da391cb..08a95ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,6 +28,7 @@ use tokio::task::block_in_place; use crate::logging::install_tracing; use tracing::{debug, error, info}; +use wayland::WaylandClient; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -47,6 +48,8 @@ async fn main() -> Result<()> { info!("Ironbar version {}", VERSION); info!("Starting application"); + let wayland_client = wayland::get_client().await; + let app = Application::builder() .application_id("dev.jstanger.ironbar") .build(); @@ -70,7 +73,7 @@ async fn main() -> Result<()> { }; debug!("Loaded config file"); - if let Err(err) = await_sync(create_bars(app, &display, &config)) { + if let Err(err) = await_sync(create_bars(app, &display, wayland_client, &config)) { error!("{:?}", err); exit(2); } @@ -100,8 +103,8 @@ async fn main() -> Result<()> { } /// Creates each of the bars across each of the (configured) outputs. -async fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<()> { - let outputs = wayland::get_output_names(); +async fn create_bars(app: &Application, display: &Display, wl: &WaylandClient, config: &Config) -> Result<()> { + let outputs = wl.outputs.as_slice(); debug!("Received {} outputs from Wayland", outputs.len()); debug!("Output names: {:?}", outputs); @@ -110,7 +113,8 @@ async fn create_bars(app: &Application, display: &Display, config: &Config) -> R for i in 0..num_monitors { let monitor = display.monitor(i).ok_or_else(|| Report::msg("GTK and Sway are reporting a different number of outputs - this is a severe bug and should never happen"))?; - let monitor_name = outputs.get(i as usize).ok_or_else(|| Report::msg("GTK and Sway are reporting a different set of outputs - this is a severe bug and should never happen"))?; + let output = outputs.get(i as usize).ok_or_else(|| Report::msg("GTK and Sway are reporting a different set of outputs - this is a severe bug and should never happen"))?; + let monitor_name = &output.name; info!("Creating bar on '{}'", monitor_name); diff --git a/src/modules/focused.rs b/src/modules/focused.rs index e1cc5f7..4d5c8d6 100644 --- a/src/modules/focused.rs +++ b/src/modules/focused.rs @@ -1,16 +1,13 @@ use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; -use crate::sway::node::{get_node_id, get_open_windows}; -use crate::sway::{get_client, get_sub_client}; -use crate::{await_sync, icon}; +use crate::wayland::{ToplevelChange}; +use crate::{await_sync, icon, wayland}; use color_eyre::Result; use glib::Continue; use gtk::prelude::*; use gtk::{IconTheme, Image, Label, Orientation}; use serde::Deserialize; -use swayipc_async::WindowChange; use tokio::spawn; use tokio::sync::mpsc::{Receiver, Sender}; -use tracing::trace; #[derive(Debug, Deserialize, Clone)] pub struct FocusedModule { @@ -43,50 +40,42 @@ impl Module for FocusedModule { _rx: Receiver, ) -> Result<()> { let focused = await_sync(async { - let sway = get_client().await; - let mut sway = sway.lock().await; - get_open_windows(&mut sway) - .await - .expect("Failed to get open windows") - .into_iter() - .find(|node| node.focused) + let wl = wayland::get_client().await; + let toplevels = wl + .toplevels + .read() + .expect("Failed to get read lock on toplevels") + .clone(); + + toplevels.into_iter().find(|top| top.active) }); - if let Some(node) = focused { - let id = get_node_id(&node); - let name = node.name.as_deref().unwrap_or(id); - + if let Some(top) = focused { tx.try_send(ModuleUpdateEvent::Update(( - name.to_string(), - id.to_string(), + top.title.clone(), + top.app_id )))?; } spawn(async move { - let mut srx = { - let sway = get_sub_client(); - sway.subscribe_window() + let mut wlrx = { + let wl = wayland::get_client().await; + wl.subscribe_toplevels() }; - trace!("Set up Sway window subscription"); - - while let Ok(payload) = srx.recv().await { - let update = match payload.change { - WindowChange::Focus => true, - WindowChange::Title => payload.container.focused, - _ => false, + while let Ok(event) = wlrx.recv().await { + let update = match event.change { + ToplevelChange::Focus(focus) => focus, + ToplevelChange::Title(_) => event.toplevel.active, + _ => false }; if update { - let node = payload.container; - - let id = get_node_id(&node); - let name = node.name.as_deref().unwrap_or(id); - - tx.try_send(ModuleUpdateEvent::Update(( - name.to_string(), - id.to_string(), + tx.send(ModuleUpdateEvent::Update(( + event.toplevel.title, + event.toplevel.app_id, ))) + .await .expect("Failed to send focus update"); } } diff --git a/src/wayland.rs b/src/wayland.rs deleted file mode 100644 index 6159038..0000000 --- a/src/wayland.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::cell::RefCell; -use std::rc::Rc; -use wayland_client::protocol::wl_output::{self, Event}; -use wayland_client::{global_filter, Display as WlDisplay, GlobalManager, Main}; - -pub fn get_output_names() -> Vec { - // Connect to the server - let display = WlDisplay::connect_to_env().unwrap(); - - let mut event_queue = display.create_event_queue(); - - let attached_display = (*display).clone().attach(event_queue.token()); - - let outputs = Rc::new(RefCell::new(Vec::::new())); - - let _globals = { - let outputs = outputs.clone(); - GlobalManager::new_with_cb(&attached_display, { - global_filter!([ - wl_output::WlOutput, - 4, - move |output: Main, _: DispatchData| { - let outputs = outputs.clone(); - - output.quick_assign(move |_, event, _| match event { - Event::Name { name: title } => { - let outputs = outputs.clone(); - outputs.as_ref().borrow_mut().push(title); - } - _ => {} - }) - } - ]) - }) - }; - - // A roundtrip synchronization to make sure the server received our registry - // creation and sent us the global list - event_queue - .sync_roundtrip(&mut (), |_, _, _| unreachable!()) - .unwrap(); - - // for some reason we need to call this twice? - event_queue - .sync_roundtrip(&mut (), |_, _, _| unreachable!()) - .unwrap(); - - outputs.take() -} diff --git a/src/wayland/client.rs b/src/wayland/client.rs new file mode 100644 index 0000000..fb6207b --- /dev/null +++ b/src/wayland/client.rs @@ -0,0 +1,108 @@ +use std::sync::{Arc, RwLock}; +use super::{Env, ToplevelHandler}; +use crate::wayland::toplevel_manager::listen_for_toplevels; +use smithay_client_toolkit::environment::Environment; +use smithay_client_toolkit::output::{with_output_info, OutputInfo}; +use smithay_client_toolkit::reexports::calloop; +use smithay_client_toolkit::{new_default_environment, WaylandSource}; +use tokio::sync::{broadcast, oneshot}; +use tokio::task::spawn_blocking; +use tracing::{trace}; +use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1; +use crate::collection::Collection; +use crate::wayland::toplevel::{ToplevelEvent, ToplevelInfo}; +use crate::wayland::ToplevelChange; + +pub struct WaylandClient { + pub outputs: Vec, + pub toplevels: Arc>>, + toplevel_tx: broadcast::Sender, + _toplevel_rx: broadcast::Receiver, +} + +impl WaylandClient { + pub(super) async fn new() -> Self { + let (output_tx, output_rx) = oneshot::channel(); + let (toplevel_tx, toplevel_rx) = broadcast::channel(32); + + let toplevel_tx2 = toplevel_tx.clone(); + + let toplevels = Arc::new(RwLock::new(Collection::new())); + let toplevels2 = toplevels.clone(); + + // `queue` is not send so we need to handle everything inside the task + spawn_blocking(move || { + let (env, _display, queue) = + new_default_environment!(Env, fields = [toplevel: ToplevelHandler::init()]) + .expect("Failed to connect to Wayland compositor"); + + let outputs = Self::get_outputs(&env); + output_tx + .send(outputs) + .expect("Failed to send outputs out of task"); + + let _toplevel_manager = env.require_global::(); + + let _listener = listen_for_toplevels(env, move |_handle, event, _ddata| { + trace!("Received toplevel event: {:?}", event); + + if event.change != ToplevelChange::Close { + toplevels2 + .write() + .expect("Failed to get write lock on toplevels") + .insert(event.toplevel.app_id.clone(), event.toplevel.clone()); + } else { + toplevels2 + .write() + .expect("Failed to get write lock on toplevels") + .remove(&event.toplevel.app_id); + } + + toplevel_tx2 + .send(event) + .expect("Failed to send toplevel event"); + }); + + let mut event_loop = calloop::EventLoop::<()>::try_new().unwrap(); + WaylandSource::new(queue) + .quick_insert(event_loop.handle()) + .unwrap(); + + loop { + event_loop.dispatch(None, &mut ()).unwrap(); + } + }); + + let outputs = output_rx + .await + .expect("Failed to receive outputs from task"); + + // spawn(async move { + // println!("start"); + // while let Ok(ev) = toplevel_rx.recv().await { + // println!("recv {:?}", ev) + // } + // println!("stop"); + // }); + + Self { + outputs, + toplevels, + toplevel_tx, + _toplevel_rx: toplevel_rx, + } + } + + pub fn subscribe_toplevels(&self) -> broadcast::Receiver { + self.toplevel_tx.subscribe() + } + + fn get_outputs(env: &Environment) -> Vec { + let outputs = env.get_all_outputs(); + + outputs + .iter() + .filter_map(|output| with_output_info(output, |info| info.clone())) + .collect() + } +} diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs new file mode 100644 index 0000000..c89ea67 --- /dev/null +++ b/src/wayland/mod.rs @@ -0,0 +1,54 @@ +mod client; +mod toplevel; +mod toplevel_manager; + +extern crate smithay_client_toolkit as sctk; + +use self::toplevel_manager::ToplevelHandler; +pub use crate::wayland::toplevel::{ToplevelChange, ToplevelEvent, ToplevelInfo}; +use crate::wayland::toplevel_manager::{ToplevelHandling, ToplevelStatusListener}; +use async_once::AsyncOnce; +use lazy_static::lazy_static; +use wayland_client::{Attached, DispatchData, Interface}; +use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{ + zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, + zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1, +}; + +pub use client::WaylandClient; + +/// A utility for lazy-loading globals. +/// Taken from smithay_client_toolkit where it's not exposed +#[derive(Debug)] +enum LazyGlobal { + Unknown, + Seen { id: u32, version: u32 }, + Bound(Attached), +} + +sctk::default_environment!(Env, + fields = [ + toplevel: ToplevelHandler + ], + singles = [ + ZwlrForeignToplevelManagerV1 => toplevel + ], +); + +impl ToplevelHandling for Env { + fn listen(&mut self, f: F) -> ToplevelStatusListener + where + F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static, + { + self.toplevel.listen(f) + } +} + +lazy_static! { + static ref CLIENT: AsyncOnce = + AsyncOnce::new(async { WaylandClient::new().await }); +} + +pub async fn get_client() -> &'static WaylandClient { + CLIENT.get().await +} diff --git a/src/wayland/toplevel.rs b/src/wayland/toplevel.rs new file mode 100644 index 0000000..e3d4485 --- /dev/null +++ b/src/wayland/toplevel.rs @@ -0,0 +1,126 @@ +use std::collections::HashSet; +use std::sync::{Arc, RwLock}; +use wayland_client::{DispatchData, Main}; +use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{Event, ZwlrForeignToplevelHandleV1}; + +const STATE_ACTIVE: u32 = 2; +const STATE_FULLSCREEN: u32 = 3; + +#[derive(Debug, Clone, Default)] +pub struct ToplevelInfo { + pub app_id: String, + pub title: String, + pub active: bool, + pub fullscreen: bool, + + ready: bool, +} + +pub struct Toplevel; + +#[derive(Debug, Clone)] +pub struct ToplevelEvent { + pub toplevel: ToplevelInfo, + pub change: ToplevelChange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ToplevelChange { + New, + Close, + Title(String), + Focus(bool), + Fullscreen(bool), +} + +fn toplevel_implem(event: Event, info: &mut ToplevelInfo, implem: &mut F, ddata: DispatchData) +where + F: FnMut(ToplevelEvent, DispatchData), +{ + let change = match event { + Event::AppId { app_id } => { + info.app_id = app_id; + None + } + Event::Title { title } => { + info.title = title.clone(); + + if info.ready { + Some(ToplevelChange::Title(title)) + } else { + None + } + } + Event::State { state } => { + // state is received as a `Vec` where every 4 bytes make up a `u32` + // the u32 then represents a value in the `State` enum. + assert_eq!(state.len() % 4, 0); + + let state = (0..state.len() / 4) + .map(|i| { + let slice: [u8; 4] = state[i * 4..i * 4 + 4] + .try_into() + .expect("Received invalid state length"); + u32::from_le_bytes(slice) + }) + .collect::>(); + + let new_active = state.contains(&STATE_ACTIVE); + let new_fullscreen = state.contains(&STATE_FULLSCREEN); + + let change = if info.ready && new_active != info.active { + Some(ToplevelChange::Focus(new_active)) + } else if info.ready && new_fullscreen != info.fullscreen { + Some(ToplevelChange::Fullscreen(new_fullscreen)) + } else { + None + }; + + info.active = new_active; + info.fullscreen = new_fullscreen; + + change + } + Event::Closed => Some(ToplevelChange::Close), + Event::OutputEnter { output: _ } => None, + Event::OutputLeave { output: _ } => None, + Event::Parent { parent: _ } => None, + Event::Done => { + assert_ne!(info.app_id, ""); + if !info.ready { + info.ready = true; + Some(ToplevelChange::New) + } else { + None + } + } + _ => unreachable!(), + }; + + if let Some(change) = change { + let event = ToplevelEvent { + change, + toplevel: info.clone(), + }; + + implem(event, ddata); + } +} + +impl Toplevel { + pub fn init(handle: Main, mut callback: F) -> Self + where + F: FnMut(ToplevelEvent, DispatchData) + 'static, + { + let inner = Arc::new(RwLock::new(ToplevelInfo::default())); + + handle.quick_assign(move |_handle, event, ddata| { + let mut inner = inner + .write() + .expect("Failed to get write lock on toplevel inner state"); + toplevel_implem(event, &mut *inner, &mut callback, ddata); + }); + + Self + } +} diff --git a/src/wayland/toplevel_manager.rs b/src/wayland/toplevel_manager.rs new file mode 100644 index 0000000..ce8e9c2 --- /dev/null +++ b/src/wayland/toplevel_manager.rs @@ -0,0 +1,166 @@ +use crate::wayland::toplevel::{Toplevel, ToplevelEvent}; +use crate::wayland::LazyGlobal; +use smithay_client_toolkit::environment::{Environment, GlobalHandler}; +use std::cell::RefCell; +use std::rc; +use std::rc::Rc; +use tracing::warn; +use wayland_client::protocol::wl_registry::WlRegistry; +use wayland_client::{Attached, DispatchData}; +use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{ + zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, + zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1}, +}; + + +struct ToplevelHandlerInner { + manager: LazyGlobal, + registry: Option>, + toplevels: Vec, +} + +impl ToplevelHandlerInner { + fn new() -> Self { + let toplevels = vec![]; + + Self { + registry: None, + manager: LazyGlobal::Unknown, + toplevels, + } + } +} + +pub struct ToplevelHandler { + inner: Rc>, + status_listeners: Rc>>>>, +} + +impl ToplevelHandler { + pub fn init() -> Self { + let inner = Rc::new(RefCell::new(ToplevelHandlerInner::new())); + + Self { + inner, + status_listeners: Rc::new(RefCell::new(Vec::new())), + } + } +} + +impl GlobalHandler for ToplevelHandler { + fn created( + &mut self, + registry: Attached, + id: u32, + version: u32, + _ddata: DispatchData, + ) { + let mut inner = RefCell::borrow_mut(&self.inner); + if inner.registry.is_none() { + inner.registry = Some(registry); + } + if let LazyGlobal::Unknown = inner.manager { + inner.manager = LazyGlobal::Seen { id, version } + } else { + warn!( + "Compositor advertised zwlr_foreign_toplevel_manager_v1 multiple times, ignoring." + ) + } + } + + fn get(&self) -> Option> { + let mut inner = RefCell::borrow_mut(&self.inner); + + match inner.manager { + LazyGlobal::Bound(ref mgr) => Some(mgr.clone()), + LazyGlobal::Unknown => None, + LazyGlobal::Seen { id, version } => { + let registry = inner.registry.as_ref().expect("Failed to get registry"); + // current max protocol version = 3 + let version = std::cmp::min(version, 3); + let manager = registry.bind::(version, id); + + { + let inner = self.inner.clone(); + let status_listeners = self.status_listeners.clone(); + + manager.quick_assign(move |_, event, _ddata| { + let mut inner = RefCell::borrow_mut(&inner); + let status_listeners = status_listeners.clone(); + + match event { + zwlr_foreign_toplevel_manager_v1::Event::Toplevel { + toplevel: handle, + } => { + let toplevel = Toplevel::init(handle.clone(), move |event, ddata| { + notify_status_listeners( + &handle, + event, + ddata, + &status_listeners, + ); + }); + + inner.toplevels.push(toplevel); + } + zwlr_foreign_toplevel_manager_v1::Event::Finished => {} + _ => unreachable!(), + } + }); + } + + inner.manager = LazyGlobal::Bound((*manager).clone()); + Some((*manager).clone()) + } + } + } +} + +type ToplevelStatusCallback = + dyn FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static; + +/// Notifies the callbacks of an event on the toplevel +fn notify_status_listeners( + toplevel: &ZwlrForeignToplevelHandleV1, + event: ToplevelEvent, + mut ddata: DispatchData, + listeners: &RefCell>>>, +) { + listeners.borrow_mut().retain(|lst| { + if let Some(cb) = rc::Weak::upgrade(lst) { + (cb.borrow_mut())(toplevel.clone(), event.clone(), ddata.reborrow()); + true + } else { + false + } + }) +} + +pub struct ToplevelStatusListener { + _cb: Rc>, +} + +pub trait ToplevelHandling { + fn listen(&mut self, f: F) -> ToplevelStatusListener + where + F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static; +} + +impl ToplevelHandling for ToplevelHandler { + fn listen(&mut self, f: F) -> ToplevelStatusListener + where + F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static, + { + let rc = Rc::new(RefCell::new(f)) as Rc<_>; + self.status_listeners.borrow_mut().push(Rc::downgrade(&rc)); + ToplevelStatusListener { _cb: rc } + } +} + +pub fn listen_for_toplevels(env: Environment, f: F) -> ToplevelStatusListener +where + E: ToplevelHandling, + F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static, +{ + env.with_inner(move |inner| ToplevelHandling::listen(inner, f)) +}