feat: dynamic lifecycle output management

This commit is contained in:
drendog 2025-11-18 22:47:15 +01:00
parent 5a1c551efb
commit 3e7bc76bb4
Signed by: dwenya
GPG key ID: 8DD77074645332D0
5 changed files with 401 additions and 10 deletions

View file

@ -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<WlOutput, ()> for AppState {
event: <WlOutput as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
qhandle: &QueueHandle<Self>,
) {
let output_id = proxy.id();
let handle = state.get_handle_by_output_id(&output_id);
@ -139,7 +141,24 @@ impl Dispatch<WlOutput, ()> 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<XdgWmBase, ()> for AppState {
_qhandle: &QueueHandle<Self>,
) {
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<XdgSurface, ()> for AppState {
}
}
impl Dispatch<WlRegistry, GlobalListContents> for AppState {
fn event(
state: &mut Self,
registry: &WlRegistry,
event: <WlRegistry as Proxy>::Event,
_data: &GlobalListContents,
_conn: &Connection,
qhandle: &QueueHandle<Self>,
) {
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::<WlOutput, _, _>(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<Self>,
) {
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, ()),

View file

@ -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<ObjectId, OutputHandle>,
}
@ -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<OutputHandle> {
self.object_to_handle.remove(object_id)
}

View file

@ -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<WpFractionalScaleManagerV1>,
pub viewporter: Option<WpViewporter>,
pub render_factory: Rc<RenderContextFactory>,
pub popup_context: PopupContext,
pub pointer: Rc<WlPointer>,
pub shared_serial: Rc<SharedPointerSerial>,
pub connection: Rc<Connection>,
}
impl OutputManagerContext {
pub const fn connection(&self) -> &Rc<Connection> {
&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<HashMap<ObjectId, PendingOutput>>,
}
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<AppState>,
) -> 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<AppState>,
) -> 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<AppState>,
) -> 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<OutputHandle> {
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()
}
}

View file

@ -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<RenderContextFactory>,
popup_context: &'a PopupContext,
pointer: &'a Rc<WlPointer>,
shared_serial: &'a Rc<SharedPointerSerial>,
}
pub struct WaylandWindowingSystem {
state: AppState,
connection: Rc<Connection>,
@ -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<Rc<PopupManager>>,
layer_surfaces: Vec<Rc<ZwlrLayerSurfaceV1>>,
@ -322,7 +368,7 @@ impl WaylandWindowingSystem {
});
}
fn initialize_renderer(
pub(crate) fn initialize_renderer(
surface: &Rc<WlSurface>,
config: &WaylandWindowConfig,
render_factory: &Rc<RenderContextFactory>,

View file

@ -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<ObjectId, OutputHandle>,
_pointer: ManagedWlPointer,
shared_pointer_serial: Rc<SharedPointerSerial>,
output_manager: Option<Rc<RefCell<OutputManager>>>,
registry_name_to_output_id: HashMap<u32, ObjectId>,
}
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<RefCell<OutputManager>>) {
self.output_manager = Some(manager);
}
pub fn output_manager(&self) -> Option<Rc<RefCell<OutputManager>>> {
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<ObjectId> {
self.registry_name_to_output_id.get(&name).cloned()
}
pub fn unregister_registry_name(&mut self, name: u32) -> Option<ObjectId> {
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<PerOutputWindow> {
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)
}