From 3e7bc76bb4c18d773aac551dce8f5e81316a5549 Mon Sep 17 00:00:00 2001 From: drendog Date: Tue, 18 Nov 2025 22:47:15 +0100 Subject: [PATCH] feat: dynamic lifecycle output management --- .../wayland/event_handling/app_dispatcher.rs | 80 +++++- crates/adapters/src/wayland/outputs.rs | 6 +- .../src/wayland/outputs/output_manager.rs | 240 ++++++++++++++++++ crates/adapters/src/wayland/shell_adapter.rs | 48 +++- .../src/wayland/surfaces/app_state.rs | 37 ++- 5 files changed, 401 insertions(+), 10 deletions(-) create mode 100644 crates/adapters/src/wayland/outputs/output_manager.rs diff --git a/crates/adapters/src/wayland/event_handling/app_dispatcher.rs b/crates/adapters/src/wayland/event_handling/app_dispatcher.rs index ac3e895..edea1f2 100644 --- a/crates/adapters/src/wayland/event_handling/app_dispatcher.rs +++ b/crates/adapters/src/wayland/event_handling/app_dispatcher.rs @@ -1,7 +1,8 @@ use crate::wayland::surfaces::app_state::AppState; use crate::wayland::surfaces::display_metrics::DisplayMetrics; +use crate::wayland::surfaces::surface_state::WindowState; use layer_shika_domain::value_objects::output_info::OutputGeometry; -use log::info; +use log::{debug, info}; use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::{ zwlr_layer_shell_v1::ZwlrLayerShellV1, zwlr_layer_surface_v1::{self, ZwlrLayerSurfaceV1}, @@ -13,6 +14,7 @@ use wayland_client::{ wl_compositor::WlCompositor, wl_output::{self, WlOutput}, wl_pointer::{self, WlPointer}, + wl_registry::Event, wl_registry::WlRegistry, wl_seat::WlSeat, wl_surface::WlSurface, @@ -76,7 +78,7 @@ impl Dispatch for AppState { event: ::Event, _data: &(), _conn: &Connection, - _qhandle: &QueueHandle, + qhandle: &QueueHandle, ) { let output_id = proxy.id(); let handle = state.get_handle_by_output_id(&output_id); @@ -139,7 +141,24 @@ impl Dispatch for AppState { } } wl_output::Event::Done => { - info!("WlOutput done"); + info!("WlOutput done for output {:?}", output_id); + + if let Some(manager) = state.output_manager() { + let manager_ref = manager.borrow(); + if manager_ref.has_pending_output(&output_id) { + drop(manager_ref); + + info!( + "Output {:?} configuration complete, finalizing...", + output_id + ); + + let manager_ref = manager.borrow(); + if let Err(e) = manager_ref.finalize_output(&output_id, state, qhandle) { + info!("Failed to finalize output {:?}: {e}", output_id); + } + } + } } _ => {} } @@ -251,7 +270,6 @@ impl Dispatch for AppState { _qhandle: &QueueHandle, ) { if let xdg_wm_base::Event::Ping { serial } = event { - use crate::wayland::surfaces::surface_state::WindowState; WindowState::handle_xdg_wm_base_ping(xdg_wm_base, serial); } } @@ -330,6 +348,57 @@ impl Dispatch for AppState { } } +impl Dispatch for AppState { + fn event( + state: &mut Self, + registry: &WlRegistry, + event: ::Event, + _data: &GlobalListContents, + _conn: &Connection, + qhandle: &QueueHandle, + ) { + match event { + Event::Global { + name, + interface, + version, + } => { + if interface == "wl_output" { + info!( + "Hot-plugged output detected! Binding wl_output with name {name}, version {version}" + ); + + let output = registry.bind::(name, 4.min(version), qhandle, ()); + let output_id = output.id(); + + if let Some(manager) = state.output_manager() { + let mut manager_ref = manager.borrow_mut(); + let handle = manager_ref.register_output(output, qhandle); + info!("Registered hot-plugged output with handle {handle:?}"); + + state.register_registry_name(name, output_id); + } else { + info!("No output manager available yet (startup initialization)"); + } + } + } + Event::GlobalRemove { name } => { + info!("Registry global removed: name {name}"); + + if let Some(output_id) = state.unregister_registry_name(name) { + info!("Output with registry name {name} removed, cleaning up..."); + + if let Some(manager) = state.output_manager() { + let mut manager_ref = manager.borrow_mut(); + manager_ref.remove_output(&output_id, state); + } + } + } + _ => {} + } + } +} + macro_rules! impl_empty_dispatch_app { ($(($t:ty, $u:ty)),+) => { $( @@ -342,7 +411,7 @@ macro_rules! impl_empty_dispatch_app { _conn: &Connection, _qhandle: &QueueHandle, ) { - info!("Implement empty dispatch event for {:?}", stringify!($t)); + debug!("Implement empty dispatch event for {:?}", stringify!($t)); } } )+ @@ -350,7 +419,6 @@ macro_rules! impl_empty_dispatch_app { } impl_empty_dispatch_app!( - (WlRegistry, GlobalListContents), (WlCompositor, ()), (WlSurface, ()), (ZwlrLayerShellV1, ()), diff --git a/crates/adapters/src/wayland/outputs.rs b/crates/adapters/src/wayland/outputs.rs index 77084e5..4a3b8d0 100644 --- a/crates/adapters/src/wayland/outputs.rs +++ b/crates/adapters/src/wayland/outputs.rs @@ -2,7 +2,10 @@ use layer_shika_domain::value_objects::output_handle::OutputHandle; use std::collections::HashMap; use wayland_client::backend::ObjectId; -pub struct OutputMapping { +pub(crate) use output_manager::{OutputManager, OutputManagerContext}; +pub(crate) mod output_manager; + +pub(crate) struct OutputMapping { object_to_handle: HashMap, } @@ -23,7 +26,6 @@ impl OutputMapping { self.object_to_handle.get(object_id).copied() } - #[allow(dead_code)] pub fn remove(&mut self, object_id: &ObjectId) -> Option { self.object_to_handle.remove(object_id) } diff --git a/crates/adapters/src/wayland/outputs/output_manager.rs b/crates/adapters/src/wayland/outputs/output_manager.rs new file mode 100644 index 0000000..33f4fef --- /dev/null +++ b/crates/adapters/src/wayland/outputs/output_manager.rs @@ -0,0 +1,240 @@ +use crate::{ + errors::{LayerShikaError, Result}, + rendering::egl::context_factory::RenderContextFactory, + wayland::{ + config::{LayerSurfaceConfig, WaylandWindowConfig}, + shell_adapter::WaylandWindowingSystem, + surfaces::{ + app_state::AppState, + event_context::SharedPointerSerial, + layer_surface::{SurfaceCtx, SurfaceSetupParams}, + popup_manager::{PopupContext, PopupManager}, + surface_builder::WindowStateBuilder, + surface_state::WindowState, + }, + }, +}; +use layer_shika_domain::value_objects::{ + output_handle::OutputHandle, + output_info::OutputInfo, +}; +use log::{info, warn}; +use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_shell_v1::ZwlrLayerShellV1; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; +use wayland_client::{ + backend::ObjectId, + protocol::{wl_compositor::WlCompositor, wl_output::WlOutput, wl_pointer::WlPointer}, + Connection, Proxy, QueueHandle, +}; +use wayland_protocols::{ + wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1, + wp::viewporter::client::wp_viewporter::WpViewporter, +}; + +use super::OutputMapping; + +pub struct OutputManagerContext { + pub compositor: WlCompositor, + pub layer_shell: ZwlrLayerShellV1, + pub fractional_scale_manager: Option, + pub viewporter: Option, + pub render_factory: Rc, + pub popup_context: PopupContext, + pub pointer: Rc, + pub shared_serial: Rc, + pub connection: Rc, +} + +impl OutputManagerContext { + pub const fn connection(&self) -> &Rc { + &self.connection + } +} + +struct PendingOutput { + proxy: WlOutput, + #[allow(dead_code)] + output_id: ObjectId, + info: OutputInfo, +} + +pub struct OutputManager { + context: OutputManagerContext, + config: WaylandWindowConfig, + pub(crate) layer_surface_config: LayerSurfaceConfig, + output_mapping: OutputMapping, + pending_outputs: RefCell>, +} + +impl OutputManager { + pub(crate) fn new( + context: OutputManagerContext, + config: WaylandWindowConfig, + layer_surface_config: LayerSurfaceConfig, + ) -> Self { + Self { + context, + config, + layer_surface_config, + output_mapping: OutputMapping::new(), + pending_outputs: RefCell::new(HashMap::new()), + } + } + + pub fn register_output( + &mut self, + output: WlOutput, + _queue_handle: &QueueHandle, + ) -> OutputHandle { + let output_id = output.id(); + let handle = self.output_mapping.insert(output_id.clone()); + + info!( + "Registered new output with handle {handle:?}, id {:?}", + output_id + ); + + let info = OutputInfo::new(handle); + + self.pending_outputs.borrow_mut().insert( + output_id.clone(), + PendingOutput { + proxy: output, + output_id, + info, + }, + ); + + handle + } + + pub fn finalize_output( + &self, + output_id: &ObjectId, + app_state: &mut AppState, + queue_handle: &QueueHandle, + ) -> Result<()> { + let mut pending = self.pending_outputs.borrow_mut(); + + let Some(pending_output) = pending.remove(output_id) else { + return Err(LayerShikaError::InvalidInput { + message: format!("No pending output found for id {output_id:?}"), + }); + }; + + let handle = pending_output.info.handle(); + let mut info = pending_output.info; + + let is_primary = app_state.output_registry().is_empty(); + info.set_primary(is_primary); + + if !self.config.output_policy.should_render(&info) { + info!( + "Skipping output {:?} due to output policy (primary: {})", + output_id, is_primary + ); + return Ok(()); + } + + info!( + "Finalizing output {:?} (handle: {handle:?}, primary: {})", + output_id, is_primary + ); + + let (window, main_surface_id) = + self.create_window_for_output(&pending_output.proxy, output_id, queue_handle)?; + + app_state.add_output(output_id.clone(), main_surface_id, window); + + Ok(()) + } + + fn create_window_for_output( + &self, + output: &WlOutput, + _output_id: &ObjectId, + queue_handle: &QueueHandle, + ) -> Result<(WindowState, ObjectId)> { + let setup_params = SurfaceSetupParams { + compositor: &self.context.compositor, + output, + layer_shell: &self.context.layer_shell, + fractional_scale_manager: self.context.fractional_scale_manager.as_ref(), + viewporter: self.context.viewporter.as_ref(), + queue_handle, + layer: self.config.layer, + namespace: self.config.namespace.clone(), + }; + + let surface_ctx = SurfaceCtx::setup(&setup_params, &self.layer_surface_config); + let main_surface_id = surface_ctx.surface.id(); + + let window = WaylandWindowingSystem::initialize_renderer( + &surface_ctx.surface, + &self.config, + &self.context.render_factory, + )?; + + let mut builder = WindowStateBuilder::new() + .with_component_definition(self.config.component_definition.clone()) + .with_compilation_result(self.config.compilation_result.clone()) + .with_surface(Rc::clone(&surface_ctx.surface)) + .with_layer_surface(Rc::clone(&surface_ctx.layer_surface)) + .with_scale_factor(self.config.scale_factor) + .with_height(self.config.height) + .with_exclusive_zone(self.config.exclusive_zone) + .with_connection(Rc::clone(self.context.connection())) + .with_pointer(Rc::clone(&self.context.pointer)) + .with_window(Rc::clone(&window)); + + if let Some(fs) = &surface_ctx.fractional_scale { + builder = builder.with_fractional_scale(Rc::clone(fs)); + } + + if let Some(vp) = &surface_ctx.viewport { + builder = builder.with_viewport(Rc::clone(vp)); + } + + let mut window_state = + WindowState::new(builder).map_err(|e| LayerShikaError::WindowConfiguration { + message: e.to_string(), + })?; + + let popup_manager = Rc::new(PopupManager::new( + self.context.popup_context.clone(), + Rc::clone(window_state.display_metrics()), + )); + + window_state.set_popup_manager(Rc::clone(&popup_manager)); + window_state.set_shared_pointer_serial(Rc::clone(&self.context.shared_serial)); + + Ok((window_state, main_surface_id)) + } + + pub fn remove_output(&mut self, output_id: &ObjectId, app_state: &mut AppState) { + if let Some(handle) = self.output_mapping.remove(output_id) { + info!("Removing output {handle:?} (id: {output_id:?})"); + + if let Some(_window) = app_state.remove_output(handle) { + info!("Cleaned up window for output {handle:?}"); + } else { + warn!("No window found for output handle {handle:?}"); + } + } else { + self.pending_outputs.borrow_mut().remove(output_id); + info!("Removed pending output {output_id:?}"); + } + } + + pub fn get_handle_by_output_id(&self, output_id: &ObjectId) -> Option { + self.output_mapping.get(output_id) + } + + pub fn has_pending_output(&self, output_id: &ObjectId) -> bool { + self.pending_outputs.borrow().contains_key(output_id) + } + + pub fn pending_output_count(&self) -> usize { + self.pending_outputs.borrow().len() + } +} diff --git a/crates/adapters/src/wayland/shell_adapter.rs b/crates/adapters/src/wayland/shell_adapter.rs index ee60f23..3b71198 100644 --- a/crates/adapters/src/wayland/shell_adapter.rs +++ b/crates/adapters/src/wayland/shell_adapter.rs @@ -2,6 +2,7 @@ use crate::wayland::{ config::{LayerSurfaceConfig, WaylandWindowConfig}, globals::context::GlobalContext, managed_proxies::ManagedWlPointer, + outputs::{OutputManager, OutputManagerContext}, surfaces::layer_surface::{SurfaceCtx, SurfaceSetupParams}, surfaces::popup_manager::{PopupContext, PopupManager}, surfaces::{ @@ -34,6 +35,7 @@ use slint_interpreter::ComponentInstance; use smithay_client_toolkit::reexports::calloop::{ EventLoop, Interest, LoopHandle, Mode, PostAction, generic::Generic, }; +use std::cell::RefCell; use std::rc::Rc; use wayland_client::{ Connection, EventQueue, Proxy, QueueHandle, @@ -50,6 +52,17 @@ struct OutputSetup { builder: WindowStateBuilder, } +struct OutputManagerParams<'a> { + config: &'a WaylandWindowConfig, + global_ctx: &'a GlobalContext, + connection: &'a Connection, + layer_surface_config: LayerSurfaceConfig, + render_factory: &'a Rc, + popup_context: &'a PopupContext, + pointer: &'a Rc, + shared_serial: &'a Rc, +} + pub struct WaylandWindowingSystem { state: AppState, connection: Rc, @@ -266,9 +279,42 @@ impl WaylandWindowingSystem { &shared_serial, ); + let output_manager = Self::create_output_manager(&OutputManagerParams { + config, + global_ctx: &global_ctx, + connection, + layer_surface_config, + render_factory: &render_factory, + popup_context: &popup_context, + pointer: &pointer, + shared_serial: &shared_serial, + }); + + app_state.set_output_manager(Rc::new(RefCell::new(output_manager))); + Ok(app_state) } + fn create_output_manager(params: &OutputManagerParams<'_>) -> OutputManager { + let manager_context = OutputManagerContext { + compositor: params.global_ctx.compositor.clone(), + layer_shell: params.global_ctx.layer_shell.clone(), + fractional_scale_manager: params.global_ctx.fractional_scale_manager.clone(), + viewporter: params.global_ctx.viewporter.clone(), + render_factory: Rc::clone(params.render_factory), + popup_context: params.popup_context.clone(), + pointer: Rc::clone(params.pointer), + shared_serial: Rc::clone(params.shared_serial), + connection: Rc::new(params.connection.clone()), + }; + + OutputManager::new( + manager_context, + params.config.clone(), + params.layer_surface_config, + ) + } + fn setup_shared_popup_creator( popup_managers: Vec>, layer_surfaces: Vec>, @@ -322,7 +368,7 @@ impl WaylandWindowingSystem { }); } - fn initialize_renderer( + pub(crate) fn initialize_renderer( surface: &Rc, config: &WaylandWindowConfig, render_factory: &Rc, diff --git a/crates/adapters/src/wayland/surfaces/app_state.rs b/crates/adapters/src/wayland/surfaces/app_state.rs index 5809a74..59cc096 100644 --- a/crates/adapters/src/wayland/surfaces/app_state.rs +++ b/crates/adapters/src/wayland/surfaces/app_state.rs @@ -1,10 +1,11 @@ use super::event_context::SharedPointerSerial; use super::surface_state::WindowState; use crate::wayland::managed_proxies::ManagedWlPointer; -use crate::wayland::outputs::OutputMapping; +use crate::wayland::outputs::{OutputManager, OutputMapping}; use layer_shika_domain::entities::output_registry::OutputRegistry; use layer_shika_domain::value_objects::output_handle::OutputHandle; use layer_shika_domain::value_objects::output_info::OutputInfo; +use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use wayland_client::Proxy; @@ -19,6 +20,8 @@ pub struct AppState { surface_to_output: HashMap, _pointer: ManagedWlPointer, shared_pointer_serial: Rc, + output_manager: Option>>, + registry_name_to_output_id: HashMap, } impl AppState { @@ -30,9 +33,31 @@ impl AppState { surface_to_output: HashMap::new(), _pointer: pointer, shared_pointer_serial: shared_serial, + output_manager: None, + registry_name_to_output_id: HashMap::new(), } } + pub fn set_output_manager(&mut self, manager: Rc>) { + self.output_manager = Some(manager); + } + + pub fn output_manager(&self) -> Option>> { + self.output_manager.as_ref().map(Rc::clone) + } + + pub fn register_registry_name(&mut self, name: u32, output_id: ObjectId) { + self.registry_name_to_output_id.insert(name, output_id); + } + + pub fn find_output_id_by_registry_name(&self, name: u32) -> Option { + self.registry_name_to_output_id.get(&name).cloned() + } + + pub fn unregister_registry_name(&mut self, name: u32) -> Option { + self.registry_name_to_output_id.remove(&name) + } + pub fn add_output( &mut self, output_id: ObjectId, @@ -51,6 +76,16 @@ impl AppState { self.windows.insert(handle, window); } + pub fn remove_output(&mut self, handle: OutputHandle) -> Option { + self.output_registry.remove(handle); + + let window = self.windows.remove(&handle); + + self.surface_to_output.retain(|_, h| *h != handle); + + window + } + pub fn get_output_by_handle(&self, handle: OutputHandle) -> Option<&PerOutputWindow> { self.windows.get(&handle) }