From 736d4d09059079b9d3e17a2fab33d1c6ab7afc88 Mon Sep 17 00:00:00 2001 From: drendog Date: Wed, 3 Dec 2025 03:18:15 +0100 Subject: [PATCH] feat: introducing base multi-window support --- crates/adapters/src/lib.rs | 6 +- crates/adapters/src/wayland/config.rs | 39 +- .../wayland/event_handling/app_dispatcher.rs | 43 +- .../src/wayland/outputs/output_manager.rs | 13 +- crates/adapters/src/wayland/shell_adapter.rs | 195 +++++++- .../src/wayland/surfaces/app_state.rs | 251 ++++++++-- .../src/wayland/surfaces/layer_surface.rs | 3 +- .../src/wayland/surfaces/rendering_state.rs | 4 + .../src/wayland/surfaces/surface_builder.rs | 8 + .../src/wayland/surfaces/surface_state.rs | 7 +- .../src/wayland/surfaces/window_renderer.rs | 10 +- crates/composition/src/builder.rs | 16 +- crates/composition/src/lib.rs | 17 + crates/composition/src/shell.rs | 449 ++++++++++++++++++ crates/composition/src/shell_composition.rs | 83 ++++ crates/composition/src/system.rs | 24 +- crates/domain/src/config.rs | 6 +- crates/domain/src/prelude.rs | 2 +- crates/domain/src/value_objects/anchor.rs | 10 + crates/domain/src/value_objects/dimensions.rs | 50 +- src/lib.rs | 26 + src/prelude.rs | 13 +- 22 files changed, 1154 insertions(+), 121 deletions(-) create mode 100644 crates/composition/src/shell.rs create mode 100644 crates/composition/src/shell_composition.rs diff --git a/crates/adapters/src/lib.rs b/crates/adapters/src/lib.rs index bb6f5b8..c2f47eb 100644 --- a/crates/adapters/src/lib.rs +++ b/crates/adapters/src/lib.rs @@ -6,7 +6,7 @@ pub(crate) mod wayland; pub use rendering::femtovg::popup_window::PopupWindow; -pub use wayland::config::WaylandWindowConfig; +pub use wayland::config::{MultiWindowConfig, ShellWindowConfig, WaylandWindowConfig}; pub use wayland::facade::WindowingSystemFacade; pub use wayland::shell_adapter::WaylandWindowingSystem; pub use wayland::surfaces::app_state::AppState; @@ -25,4 +25,8 @@ pub mod platform { EventSource, InsertError, Interest, Mode, PostAction, RegistrationToken, }; } + + pub mod wayland { + pub use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::Anchor; + } } diff --git a/crates/adapters/src/wayland/config.rs b/crates/adapters/src/wayland/config.rs index 4197560..9f753de 100644 --- a/crates/adapters/src/wayland/config.rs +++ b/crates/adapters/src/wayland/config.rs @@ -18,11 +18,13 @@ pub(crate) struct LayerSurfaceConfig { pub exclusive_zone: i32, pub keyboard_interactivity: WaylandKeyboardInteractivity, pub height: u32, + pub width: u32, } #[derive(Clone)] pub struct WaylandWindowConfig { pub height: u32, + pub width: u32, pub layer: zwlr_layer_shell_v1::Layer, pub margin: Margins, pub anchor: Anchor, @@ -43,7 +45,8 @@ impl WaylandWindowConfig { domain_config: DomainWindowConfig, ) -> Self { Self { - height: domain_config.height.value(), + height: domain_config.dimensions.height(), + width: domain_config.dimensions.width(), layer: convert_layer(domain_config.layer), margin: domain_config.margin, anchor: convert_anchor(domain_config.anchor), @@ -97,3 +100,37 @@ const fn convert_keyboard_interactivity( DomainKeyboardInteractivity::OnDemand => WaylandKeyboardInteractivity::OnDemand, } } + +#[derive(Clone)] +pub struct ShellWindowConfig { + pub name: String, + pub config: WaylandWindowConfig, +} + +#[derive(Clone)] +pub struct MultiWindowConfig { + pub windows: Vec, + pub compilation_result: Rc, +} + +impl MultiWindowConfig { + pub fn new(compilation_result: Rc) -> Self { + Self { + windows: Vec::new(), + compilation_result, + } + } + + #[must_use] + pub fn add_window(mut self, name: impl Into, config: WaylandWindowConfig) -> Self { + self.windows.push(ShellWindowConfig { + name: name.into(), + config, + }); + self + } + + pub fn primary_config(&self) -> Option<&WaylandWindowConfig> { + self.windows.first().map(|w| &w.config) + } +} diff --git a/crates/adapters/src/wayland/event_handling/app_dispatcher.rs b/crates/adapters/src/wayland/event_handling/app_dispatcher.rs index 7175e49..f56d2bc 100644 --- a/crates/adapters/src/wayland/event_handling/app_dispatcher.rs +++ b/crates/adapters/src/wayland/event_handling/app_dispatcher.rs @@ -98,7 +98,7 @@ impl Dispatch for AppState { width, height, is_current, is_preferred ); if is_current { - if let Some(window) = state.get_output_by_output_id_mut(&output_id) { + for window in state.all_windows_for_output_mut(&output_id) { window.handle_output_mode(width, height); } } @@ -202,21 +202,16 @@ impl Dispatch for AppState { let surface_id = surface.id(); - if let Some(window) = state.get_output_by_surface_mut(&surface_id) { - window.handle_pointer_enter(serial, &surface, surface_x, surface_y); - - if let Some(handle) = state.get_handle_by_surface(&surface_id) { - state.set_active_output_handle(Some(handle)); - } - } else { - let handle = state.get_handle_by_popup(&surface_id); - if let Some(window) = state.find_output_by_popup_mut(&surface_id) { + if let Some(key) = state.get_key_by_surface(&surface_id).cloned() { + if let Some(window) = state.get_window_by_key_mut(&key) { window.handle_pointer_enter(serial, &surface, surface_x, surface_y); - - if let Some(handle) = handle { - state.set_active_output_handle(Some(handle)); - } } + state.set_active_window_key(Some(key)); + } else if let Some(key) = state.get_key_by_popup(&surface_id).cloned() { + if let Some(window) = state.get_window_by_key_mut(&key) { + window.handle_pointer_enter(serial, &surface, surface_x, surface_y); + } + state.set_active_window_key(Some(key)); } } @@ -225,20 +220,16 @@ impl Dispatch for AppState { surface_y, .. } => { - if let Some(output_handle) = state.active_output_handle() { - if let Some(window) = state.get_output_by_handle_mut(output_handle) { - window.handle_pointer_motion(surface_x, surface_y); - } + if let Some(window) = state.active_window_mut() { + window.handle_pointer_motion(surface_x, surface_y); } } wl_pointer::Event::Leave { .. } => { - if let Some(output_handle) = state.active_output_handle() { - if let Some(window) = state.get_output_by_handle_mut(output_handle) { - window.handle_pointer_leave(); - } + if let Some(window) = state.active_window_mut() { + window.handle_pointer_leave(); } - state.set_active_output_handle(None); + state.set_active_window_key(None); } wl_pointer::Event::Button { @@ -246,10 +237,8 @@ impl Dispatch for AppState { state: button_state, .. } => { - if let Some(output_handle) = state.active_output_handle() { - if let Some(window) = state.get_output_by_handle_mut(output_handle) { - window.handle_pointer_button(serial, button_state); - } + if let Some(window) = state.active_window_mut() { + window.handle_pointer_button(serial, button_state); } } _ => {} diff --git a/crates/adapters/src/wayland/outputs/output_manager.rs b/crates/adapters/src/wayland/outputs/output_manager.rs index 33f4fef..bcdc555 100644 --- a/crates/adapters/src/wayland/outputs/output_manager.rs +++ b/crates/adapters/src/wayland/outputs/output_manager.rs @@ -144,7 +144,7 @@ impl OutputManager { 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); + app_state.add_output(output_id, main_surface_id, window); Ok(()) } @@ -182,6 +182,7 @@ impl OutputManager { .with_layer_surface(Rc::clone(&surface_ctx.layer_surface)) .with_scale_factor(self.config.scale_factor) .with_height(self.config.height) + .with_width(self.config.width) .with_exclusive_zone(self.config.exclusive_zone) .with_connection(Rc::clone(self.context.connection())) .with_pointer(Rc::clone(&self.context.pointer)) @@ -215,10 +216,14 @@ impl OutputManager { 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 { + let removed_windows = app_state.remove_output(handle); + if removed_windows.is_empty() { warn!("No window found for output handle {handle:?}"); + } else { + info!( + "Cleaned up {} window(s) for output {handle:?}", + removed_windows.len() + ); } } else { self.pending_outputs.borrow_mut().remove(output_id); diff --git a/crates/adapters/src/wayland/shell_adapter.rs b/crates/adapters/src/wayland/shell_adapter.rs index 27df0c6..532ff8c 100644 --- a/crates/adapters/src/wayland/shell_adapter.rs +++ b/crates/adapters/src/wayland/shell_adapter.rs @@ -1,5 +1,5 @@ use crate::wayland::{ - config::{LayerSurfaceConfig, WaylandWindowConfig}, + config::{LayerSurfaceConfig, ShellWindowConfig, WaylandWindowConfig}, globals::context::GlobalContext, managed_proxies::ManagedWlPointer, outputs::{OutputManager, OutputManagerContext}, @@ -50,6 +50,7 @@ struct OutputSetup { main_surface_id: ObjectId, window: Rc, builder: WindowStateBuilder, + shell_window_name: String, } struct OutputManagerParams<'a> { @@ -87,6 +88,31 @@ impl WaylandWindowingSystem { }) } + pub fn new_multi(configs: &[ShellWindowConfig]) -> Result { + if configs.is_empty() { + return Err(LayerShikaError::InvalidInput { + message: "At least one window config is required".into(), + }); + } + + info!( + "Initializing WindowingSystem with {} window configs", + configs.len() + ); + let (connection, mut event_queue) = Self::init_wayland_connection()?; + let event_loop = + EventLoop::try_new().map_err(|e| EventLoopError::Creation { source: e })?; + + let state = Self::init_state_multi(configs, &connection, &mut event_queue)?; + + Ok(Self { + state, + connection, + event_queue, + event_loop, + }) + } + fn init_wayland_connection() -> Result<(Rc, EventQueue)> { let connection = Rc::new(Connection::connect_to_env()?); let event_queue = connection.new_event_queue(); @@ -100,6 +126,7 @@ impl WaylandWindowingSystem { exclusive_zone: config.exclusive_zone, keyboard_interactivity: config.keyboard_interactivity, height: config.height, + width: config.width, } } @@ -152,6 +179,7 @@ impl WaylandWindowingSystem { .with_layer_surface(Rc::clone(&surface_ctx.layer_surface)) .with_scale_factor(config.scale_factor) .with_height(config.height) + .with_width(config.width) .with_exclusive_zone(config.exclusive_zone) .with_connection(Rc::new(connection.clone())) .with_pointer(Rc::clone(pointer)) @@ -170,6 +198,7 @@ impl WaylandWindowingSystem { main_surface_id, window, builder, + shell_window_name: "default".to_string(), }); } @@ -222,7 +251,12 @@ impl WaylandWindowingSystem { popup_managers.push(Rc::clone(&popup_manager)); layer_surfaces.push(per_output_window.layer_surface()); - app_state.add_output(setup.output_id, setup.main_surface_id, per_output_window); + app_state.add_shell_window( + &setup.output_id, + &setup.shell_window_name, + setup.main_surface_id, + per_output_window, + ); } Ok((popup_managers, layer_surfaces)) @@ -295,6 +329,163 @@ impl WaylandWindowingSystem { Ok(app_state) } + fn init_state_multi( + configs: &[ShellWindowConfig], + connection: &Connection, + event_queue: &mut EventQueue, + ) -> Result { + let global_ctx = GlobalContext::initialize(connection, &event_queue.handle())?; + + let pointer = Rc::new(global_ctx.seat.get_pointer(&event_queue.handle(), ())); + let shared_serial = Rc::new(SharedPointerSerial::new()); + + let mut app_state = AppState::new( + ManagedWlPointer::new(Rc::clone(&pointer), Rc::new(connection.clone())), + Rc::clone(&shared_serial), + ); + + let render_factory = + RenderContextFactory::new(Rc::clone(&global_ctx.render_context_manager)); + + let popup_context = PopupContext::new( + global_ctx.compositor.clone(), + global_ctx.xdg_wm_base.clone(), + global_ctx.seat.clone(), + global_ctx.fractional_scale_manager.clone(), + global_ctx.viewporter.clone(), + Rc::new(connection.clone()), + Rc::clone(&render_factory), + ); + + let setups = Self::create_output_setups_multi( + configs, + &global_ctx, + connection, + event_queue, + &pointer, + )?; + + let platform = Self::setup_platform(&setups)?; + + let (popup_managers, layer_surfaces) = + Self::create_window_states(setups, &popup_context, &shared_serial, &mut app_state)?; + + Self::setup_shared_popup_creator( + popup_managers, + layer_surfaces, + &platform, + &event_queue.handle(), + &shared_serial, + ); + + let primary_config = configs.first().map(|c| &c.config); + if let Some(config) = primary_config { + let layer_surface_config = Self::create_layer_surface_config(config); + 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_setups_multi( + configs: &[ShellWindowConfig], + global_ctx: &GlobalContext, + connection: &Connection, + event_queue: &mut EventQueue, + pointer: &Rc, + ) -> Result> { + let mut setups = Vec::new(); + + for (output_index, output) in global_ctx.outputs.iter().enumerate() { + let is_primary = output_index == 0; + let output_id = output.id(); + + for shell_config in configs { + let config = &shell_config.config; + + let mut temp_info = OutputInfo::new(OutputHandle::new()); + temp_info.set_primary(is_primary); + + if !config.output_policy.should_render(&temp_info) { + info!( + "Skipping shell window '{}' on output {} due to output policy", + shell_config.name, output_index + ); + continue; + } + + let layer_surface_config = Self::create_layer_surface_config(config); + + let setup_params = SurfaceSetupParams { + compositor: &global_ctx.compositor, + output, + layer_shell: &global_ctx.layer_shell, + fractional_scale_manager: global_ctx.fractional_scale_manager.as_ref(), + viewporter: global_ctx.viewporter.as_ref(), + queue_handle: &event_queue.handle(), + layer: config.layer, + namespace: config.namespace.clone(), + }; + + let surface_ctx = SurfaceCtx::setup(&setup_params, &layer_surface_config); + let main_surface_id = surface_ctx.surface.id(); + + let render_factory = + RenderContextFactory::new(Rc::clone(&global_ctx.render_context_manager)); + + let window = + Self::initialize_renderer(&surface_ctx.surface, config, &render_factory)?; + + let mut builder = WindowStateBuilder::new() + .with_component_definition(config.component_definition.clone()) + .with_compilation_result(config.compilation_result.clone()) + .with_surface(Rc::clone(&surface_ctx.surface)) + .with_layer_surface(Rc::clone(&surface_ctx.layer_surface)) + .with_scale_factor(config.scale_factor) + .with_height(config.height) + .with_width(config.width) + .with_exclusive_zone(config.exclusive_zone) + .with_connection(Rc::new(connection.clone())) + .with_pointer(Rc::clone(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)); + } + + info!( + "Created setup for shell window '{}' on output {}", + shell_config.name, output_index + ); + + setups.push(OutputSetup { + output_id: output_id.clone(), + main_surface_id, + window, + builder, + shell_window_name: shell_config.name.clone(), + }); + } + } + + Ok(setups) + } + fn create_output_manager(params: &OutputManagerParams<'_>) -> OutputManager { let manager_context = OutputManagerContext { compositor: params.global_ctx.compositor.clone(), diff --git a/crates/adapters/src/wayland/surfaces/app_state.rs b/crates/adapters/src/wayland/surfaces/app_state.rs index 674b280..b5d60b0 100644 --- a/crates/adapters/src/wayland/surfaces/app_state.rs +++ b/crates/adapters/src/wayland/surfaces/app_state.rs @@ -13,15 +13,31 @@ use wayland_client::backend::ObjectId; pub type PerOutputWindow = WindowState; +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ShellWindowKey { + pub output_handle: OutputHandle, + pub shell_window_name: String, +} + +impl ShellWindowKey { + pub fn new(output_handle: OutputHandle, shell_window_name: impl Into) -> Self { + Self { + output_handle, + shell_window_name: shell_window_name.into(), + } + } +} + pub struct AppState { output_registry: OutputRegistry, output_mapping: OutputMapping, - windows: HashMap, - surface_to_output: HashMap, + windows: HashMap, + surface_to_key: HashMap, _pointer: ManagedWlPointer, shared_pointer_serial: Rc, output_manager: Option>>, registry_name_to_output_id: HashMap, + active_window_key: Option, } impl AppState { @@ -30,11 +46,12 @@ impl AppState { output_registry: OutputRegistry::new(), output_mapping: OutputMapping::new(), windows: HashMap::new(), - surface_to_output: HashMap::new(), + surface_to_key: HashMap::new(), _pointer: pointer, shared_pointer_serial: shared_serial, output_manager: None, registry_name_to_output_id: HashMap::new(), + active_window_key: None, } } @@ -58,49 +75,88 @@ impl AppState { self.registry_name_to_output_id.remove(&name) } - pub fn add_output( + pub fn add_shell_window( &mut self, - output_id: ObjectId, + output_id: &ObjectId, + shell_window_name: &str, main_surface_id: ObjectId, window: PerOutputWindow, ) { - let handle = self.output_mapping.insert(output_id); - self.surface_to_output.insert(main_surface_id, handle); + let handle = self.output_mapping.get(output_id).unwrap_or_else(|| { + let h = self.output_mapping.insert(output_id.clone()); + let is_primary = self.output_registry.is_empty(); + let mut info = OutputInfo::new(h); + info.set_primary(is_primary); + self.output_registry.add(info); + h + }); - let is_primary = self.output_registry.is_empty(); - - let mut info = OutputInfo::new(handle); - info.set_primary(is_primary); - - self.output_registry.add(info); - self.windows.insert(handle, window); + let key = ShellWindowKey::new(handle, shell_window_name); + self.surface_to_key.insert(main_surface_id, key.clone()); + self.windows.insert(key, window); } - pub fn remove_output(&mut self, handle: OutputHandle) -> Option { + pub fn add_output( + &mut self, + output_id: &ObjectId, + main_surface_id: ObjectId, + window: PerOutputWindow, + ) { + self.add_shell_window(output_id, "default", main_surface_id, window); + } + + pub fn remove_output(&mut self, handle: OutputHandle) -> Vec { self.output_registry.remove(handle); - let window = self.windows.remove(&handle); + let keys_to_remove: Vec<_> = self + .windows + .keys() + .filter(|k| k.output_handle == handle) + .cloned() + .collect(); - self.surface_to_output.retain(|_, h| *h != handle); + let mut removed = Vec::new(); + for key in keys_to_remove { + if let Some(window) = self.windows.remove(&key) { + removed.push(window); + } + } - window + self.surface_to_key.retain(|_, k| k.output_handle != handle); + + removed } - pub fn get_output_by_handle(&self, handle: OutputHandle) -> Option<&PerOutputWindow> { - self.windows.get(&handle) + pub fn get_window_by_key(&self, key: &ShellWindowKey) -> Option<&PerOutputWindow> { + self.windows.get(key) } - pub fn get_output_by_handle_mut( + pub fn get_window_by_key_mut(&mut self, key: &ShellWindowKey) -> Option<&mut PerOutputWindow> { + self.windows.get_mut(key) + } + + pub fn get_window_by_name( + &self, + output_handle: OutputHandle, + shell_window_name: &str, + ) -> Option<&PerOutputWindow> { + let key = ShellWindowKey::new(output_handle, shell_window_name); + self.windows.get(&key) + } + + pub fn get_window_by_name_mut( &mut self, - handle: OutputHandle, + output_handle: OutputHandle, + shell_window_name: &str, ) -> Option<&mut PerOutputWindow> { - self.windows.get_mut(&handle) + let key = ShellWindowKey::new(output_handle, shell_window_name); + self.windows.get_mut(&key) } pub fn get_output_by_output_id(&self, output_id: &ObjectId) -> Option<&PerOutputWindow> { self.output_mapping .get(output_id) - .and_then(|handle| self.windows.get(&handle)) + .and_then(|handle| self.get_first_window_for_output(handle)) } pub fn get_output_by_output_id_mut( @@ -109,22 +165,39 @@ impl AppState { ) -> Option<&mut PerOutputWindow> { self.output_mapping .get(output_id) - .and_then(|handle| self.windows.get_mut(&handle)) + .and_then(|handle| self.get_first_window_for_output_mut(handle)) + } + + fn get_first_window_for_output(&self, handle: OutputHandle) -> Option<&PerOutputWindow> { + self.windows + .iter() + .find(|(k, _)| k.output_handle == handle) + .map(|(_, v)| v) + } + + fn get_first_window_for_output_mut( + &mut self, + handle: OutputHandle, + ) -> Option<&mut PerOutputWindow> { + self.windows + .iter_mut() + .find(|(k, _)| k.output_handle == handle) + .map(|(_, v)| v) } pub fn get_output_by_surface(&self, surface_id: &ObjectId) -> Option<&PerOutputWindow> { - self.surface_to_output + self.surface_to_key .get(surface_id) - .and_then(|handle| self.windows.get(handle)) + .and_then(|key| self.windows.get(key)) } pub fn get_output_by_surface_mut( &mut self, surface_id: &ObjectId, ) -> Option<&mut PerOutputWindow> { - self.surface_to_output + self.surface_to_key .get(surface_id) - .and_then(|handle| self.windows.get_mut(handle)) + .and_then(|key| self.windows.get_mut(key)) } pub fn get_output_by_layer_surface_mut( @@ -136,8 +209,14 @@ impl AppState { .find(|window| window.layer_surface().as_ref().id() == *layer_surface_id) } + pub fn get_key_by_surface(&self, surface_id: &ObjectId) -> Option<&ShellWindowKey> { + self.surface_to_key.get(surface_id) + } + pub fn get_handle_by_surface(&self, surface_id: &ObjectId) -> Option { - self.surface_to_output.get(surface_id).copied() + self.surface_to_key + .get(surface_id) + .map(|key| key.output_handle) } pub fn get_handle_by_output_id(&self, output_id: &ObjectId) -> Option { @@ -152,10 +231,28 @@ impl AppState { self.output_registry.active_handle() } + pub fn set_active_window_key(&mut self, key: Option) { + if let Some(ref k) = key { + self.output_registry.set_active(Some(k.output_handle)); + } else { + self.output_registry.set_active(None); + } + self.active_window_key = key; + } + + pub fn active_window_key(&self) -> Option<&ShellWindowKey> { + self.active_window_key.as_ref() + } + + pub fn active_window_mut(&mut self) -> Option<&mut PerOutputWindow> { + let key = self.active_window_key.clone()?; + self.windows.get_mut(&key) + } + pub fn primary_output(&self) -> Option<&PerOutputWindow> { self.output_registry .primary_handle() - .and_then(|handle| self.windows.get(&handle)) + .and_then(|handle| self.get_first_window_for_output(handle)) } pub fn primary_output_handle(&self) -> Option { @@ -165,7 +262,7 @@ impl AppState { pub fn active_output(&self) -> Option<&PerOutputWindow> { self.output_registry .active_handle() - .and_then(|handle| self.windows.get(&handle)) + .and_then(|handle| self.get_first_window_for_output(handle)) } pub fn all_outputs(&self) -> impl Iterator { @@ -176,10 +273,18 @@ impl AppState { self.windows.values_mut() } - pub fn outputs_with_handles(&self) -> impl Iterator { + pub fn windows_for_output( + &self, + handle: OutputHandle, + ) -> impl Iterator { self.windows .iter() - .map(|(&handle, window)| (handle, window)) + .filter(move |(k, _)| k.output_handle == handle) + .map(|(k, v)| (k.shell_window_name.as_str(), v)) + } + + pub fn windows_with_keys(&self) -> impl Iterator { + self.windows.iter() } pub const fn shared_pointer_serial(&self) -> &Rc { @@ -209,16 +314,28 @@ impl AppState { }) } - pub fn get_handle_by_popup(&self, popup_surface_id: &ObjectId) -> Option { - self.windows.iter().find_map(|(&handle, window)| { + pub fn get_key_by_popup(&self, popup_surface_id: &ObjectId) -> Option<&ShellWindowKey> { + self.windows.iter().find_map(|(key, window)| { window .popup_manager() .as_ref() .and_then(|pm| pm.find_by_surface(popup_surface_id)) - .map(|_| handle) + .map(|_| key) }) } + pub fn get_handle_by_popup(&self, popup_surface_id: &ObjectId) -> Option { + self.get_key_by_popup(popup_surface_id) + .map(|key| key.output_handle) + } + + pub fn get_output_by_handle_mut( + &mut self, + handle: OutputHandle, + ) -> Option<&mut PerOutputWindow> { + self.get_first_window_for_output_mut(handle) + } + pub fn get_output_info(&self, handle: OutputHandle) -> Option<&OutputInfo> { self.output_registry.get(handle) } @@ -231,13 +348,61 @@ impl AppState { self.output_registry.all_info() } - pub fn outputs_with_info(&self) -> impl Iterator { - self.output_registry - .all() - .filter_map(|(handle, info)| self.windows.get(&handle).map(|window| (info, window))) - } - pub const fn output_registry(&self) -> &OutputRegistry { &self.output_registry } + + pub fn shell_window_names(&self) -> Vec<&str> { + let mut names: Vec<_> = self + .windows + .keys() + .map(|k| k.shell_window_name.as_str()) + .collect(); + names.sort_unstable(); + names.dedup(); + names + } + + pub fn windows_by_shell_name( + &self, + shell_window_name: &str, + ) -> impl Iterator { + self.windows + .iter() + .filter(move |(k, _)| k.shell_window_name == shell_window_name) + .map(|(_, v)| v) + } + + pub fn get_output_by_handle(&self, handle: OutputHandle) -> Option<&PerOutputWindow> { + self.get_first_window_for_output(handle) + } + + pub fn outputs_with_handles(&self) -> impl Iterator { + self.windows + .iter() + .map(|(key, window)| (key.output_handle, window)) + } + + pub fn outputs_with_info(&self) -> impl Iterator { + self.output_registry.all_info().filter_map(|info| { + let handle = info.handle(); + self.get_first_window_for_output(handle) + .map(|window| (info, window)) + }) + } + + pub fn all_windows_for_output_mut( + &mut self, + output_id: &ObjectId, + ) -> Vec<&mut PerOutputWindow> { + let Some(handle) = self.output_mapping.get(output_id) else { + return Vec::new(); + }; + + self.windows + .iter_mut() + .filter(|(k, _)| k.output_handle == handle) + .map(|(_, v)| v) + .collect() + } } diff --git a/crates/adapters/src/wayland/surfaces/layer_surface.rs b/crates/adapters/src/wayland/surfaces/layer_surface.rs index f586a46..c90b1ff 100644 --- a/crates/adapters/src/wayland/surfaces/layer_surface.rs +++ b/crates/adapters/src/wayland/surfaces/layer_surface.rs @@ -90,7 +90,8 @@ impl SurfaceCtx { layer_surface.set_exclusive_zone(config.exclusive_zone); layer_surface.set_keyboard_interactivity(config.keyboard_interactivity); - layer_surface.set_size(1, config.height); + + layer_surface.set_size(config.width, config.height); surface.commit(); } } diff --git a/crates/adapters/src/wayland/surfaces/rendering_state.rs b/crates/adapters/src/wayland/surfaces/rendering_state.rs index 6dc929d..db35c66 100644 --- a/crates/adapters/src/wayland/surfaces/rendering_state.rs +++ b/crates/adapters/src/wayland/surfaces/rendering_state.rs @@ -49,4 +49,8 @@ impl RenderingState { pub fn fractional_scale(&self) -> Option<&ManagedWpFractionalScaleV1> { self.renderer.fractional_scale() } + + pub fn commit_surface(&self) { + self.renderer.commit_surface(); + } } diff --git a/crates/adapters/src/wayland/surfaces/surface_builder.rs b/crates/adapters/src/wayland/surfaces/surface_builder.rs index d4f4f5a..dd090a3 100644 --- a/crates/adapters/src/wayland/surfaces/surface_builder.rs +++ b/crates/adapters/src/wayland/surfaces/surface_builder.rs @@ -37,6 +37,7 @@ pub struct WindowStateBuilder { pub connection: Option>, pub scale_factor: f32, pub height: u32, + pub width: u32, pub exclusive_zone: i32, } @@ -100,6 +101,12 @@ impl WindowStateBuilder { self } + #[must_use] + pub const fn with_width(mut self, width: u32) -> Self { + self.width = width; + self + } + #[must_use] pub fn with_component_definition(mut self, component_definition: ComponentDefinition) -> Self { self.component_definition = Some(component_definition); @@ -163,6 +170,7 @@ impl Default for WindowStateBuilder { connection: None, scale_factor: 1.0, height: 30, + width: 0, exclusive_zone: -1, } } diff --git a/crates/adapters/src/wayland/surfaces/surface_state.rs b/crates/adapters/src/wayland/surfaces/surface_state.rs index a10ac3d..72841bc 100644 --- a/crates/adapters/src/wayland/surfaces/surface_state.rs +++ b/crates/adapters/src/wayland/surfaces/surface_state.rs @@ -106,7 +106,6 @@ impl WindowState { viewport, fractional_scale, height: builder.height, - exclusive_zone: builder.exclusive_zone, size, }); @@ -143,10 +142,14 @@ impl WindowState { Rc::clone(self.rendering.window()) } - pub(crate) fn layer_surface(&self) -> Rc { + pub fn layer_surface(&self) -> Rc { self.rendering.layer_surface() } + pub fn commit_surface(&self) { + self.rendering.commit_surface(); + } + pub fn height(&self) -> u32 { self.rendering.height() } diff --git a/crates/adapters/src/wayland/surfaces/window_renderer.rs b/crates/adapters/src/wayland/surfaces/window_renderer.rs index 863fede..7447d75 100644 --- a/crates/adapters/src/wayland/surfaces/window_renderer.rs +++ b/crates/adapters/src/wayland/surfaces/window_renderer.rs @@ -24,7 +24,6 @@ pub struct WindowRendererParams { pub viewport: Option, pub fractional_scale: Option, pub height: u32, - pub exclusive_zone: i32, pub size: PhysicalSize, } @@ -35,7 +34,6 @@ pub struct WindowRenderer { viewport: Option, fractional_scale: Option, height: u32, - exclusive_zone: i32, size: PhysicalSize, logical_size: PhysicalSize, } @@ -50,7 +48,6 @@ impl WindowRenderer { viewport: params.viewport, fractional_scale: params.fractional_scale, height: params.height, - exclusive_zone: params.exclusive_zone, size: params.size, logical_size: PhysicalSize::default(), } @@ -137,9 +134,6 @@ impl WindowRenderer { } } - self.layer_surface - .set_size(dimensions.logical_width(), dimensions.logical_height()); - self.layer_surface.set_exclusive_zone(self.exclusive_zone); self.surface.commit(); } @@ -191,4 +185,8 @@ impl WindowRenderer { pub const fn fractional_scale(&self) -> Option<&ManagedWpFractionalScaleV1> { self.fractional_scale.as_ref() } + + pub fn commit_surface(&self) { + self.surface.commit(); + } } diff --git a/crates/composition/src/builder.rs b/crates/composition/src/builder.rs index c05b322..e438b29 100644 --- a/crates/composition/src/builder.rs +++ b/crates/composition/src/builder.rs @@ -4,7 +4,7 @@ use layer_shika_adapters::platform::slint_interpreter::{CompilationResult, Compi use layer_shika_domain::errors::DomainError; use layer_shika_domain::prelude::{ AnchorEdges, KeyboardInteractivity, Layer, Margins, OutputPolicy, ScaleFactor, WindowConfig, - WindowHeight, + WindowDimension, }; use spin_on::spin_on; use std::path::{Path, PathBuf}; @@ -136,9 +136,21 @@ impl LayerShika { } impl LayerShika { + #[must_use] + pub fn dimensions(mut self, width: u32, height: u32) -> Self { + self.config.dimensions = WindowDimension::new(width, height); + self + } + #[must_use] pub fn height(mut self, height: u32) -> Self { - self.config.height = WindowHeight::new(height); + self.config.dimensions = WindowDimension::new(self.config.dimensions.width(), height); + self + } + + #[must_use] + pub fn width(mut self, width: u32) -> Self { + self.config.dimensions = WindowDimension::new(width, self.config.dimensions.height()); self } diff --git a/crates/composition/src/lib.rs b/crates/composition/src/lib.rs index 85e71aa..1357861 100644 --- a/crates/composition/src/lib.rs +++ b/crates/composition/src/lib.rs @@ -2,6 +2,8 @@ mod builder; mod popup_builder; +mod shell; +mod shell_composition; mod system; mod value_conversion; @@ -27,6 +29,12 @@ pub use layer_shika_domain::value_objects::popup_request::{ pub use popup_builder::PopupBuilder; pub use system::{App, EventContext, EventLoopHandle, ShellControl}; +pub use shell::{ + LayerSurfaceHandle, Shell, ShellEventContext, ShellEventLoopHandle, ShellWindowConfigHandler, + ShellWindowHandle, +}; +pub use shell_composition::{ShellComposition, ShellWindowDefinition}; + pub mod calloop { pub use layer_shika_adapters::platform::calloop::{ Generic, Interest, Mode, PostAction, RegistrationToken, TimeoutAction, Timer, channel, @@ -55,7 +63,16 @@ pub mod prelude { PopupWindow, Result, ShellControl, }; + pub use crate::{ + LayerSurfaceHandle, Shell, ShellComposition, ShellEventContext, ShellEventLoopHandle, + ShellWindowConfigHandler, ShellWindowDefinition, ShellWindowHandle, + }; + pub use crate::calloop::{Generic, Interest, Mode, PostAction, RegistrationToken, Timer}; pub use crate::{slint, slint_interpreter}; + + pub use layer_shika_domain::prelude::{Margins, ScaleFactor, WindowConfig, WindowDimension}; + + pub use layer_shika_adapters::platform::wayland::Anchor; } diff --git a/crates/composition/src/shell.rs b/crates/composition/src/shell.rs new file mode 100644 index 0000000..06d7836 --- /dev/null +++ b/crates/composition/src/shell.rs @@ -0,0 +1,449 @@ +use crate::shell_composition::ShellWindowDefinition; +use crate::system::{EventContext, PopupCommand, ShellControl}; +use crate::{Error, Result}; +use layer_shika_adapters::errors::EventLoopError; +use layer_shika_adapters::platform::calloop::{ + EventSource, Generic, Interest, Mode, PostAction, RegistrationToken, TimeoutAction, Timer, + channel, +}; +use layer_shika_adapters::platform::slint_interpreter::{CompilationResult, ComponentInstance}; +use layer_shika_adapters::platform::wayland::Anchor; +use layer_shika_adapters::{ + AppState, ShellWindowConfig, WaylandWindowConfig, WindowState, WindowingSystemFacade, +}; +use layer_shika_domain::config::WindowConfig; +use layer_shika_domain::errors::DomainError; +use std::cell::RefCell; +use std::collections::HashMap; +use std::os::unix::io::AsFd; +use std::rc::{Rc, Weak}; +use std::result::Result as StdResult; +use std::time::{Duration, Instant}; + +pub struct LayerSurfaceHandle<'a> { + window_state: &'a WindowState, +} + +impl LayerSurfaceHandle<'_> { + pub fn set_anchor(&self, anchor: Anchor) { + self.window_state.layer_surface().set_anchor(anchor); + } + + pub fn set_size(&self, width: u32, height: u32) { + self.window_state.layer_surface().set_size(width, height); + } + + pub fn set_exclusive_zone(&self, zone: i32) { + self.window_state.layer_surface().set_exclusive_zone(zone); + } + + pub fn commit(&self) { + self.window_state.commit_surface(); + } +} + +pub trait ShellWindowConfigHandler { + fn configure_window(&self, instance: &ComponentInstance, surface: LayerSurfaceHandle<'_>); +} + +impl ShellWindowConfigHandler for F +where + F: Fn(&ComponentInstance, LayerSurfaceHandle<'_>), +{ + fn configure_window(&self, instance: &ComponentInstance, surface: LayerSurfaceHandle<'_>) { + self(instance, surface); + } +} + +#[derive(Debug, Clone)] +pub struct ShellWindowHandle { + pub name: String, +} + +pub struct Shell { + inner: Rc>, + windows: HashMap, + compilation_result: Rc, + popup_command_sender: channel::Sender, +} + +impl Shell { + pub(crate) fn new( + compilation_result: Rc, + definitions: &[ShellWindowDefinition], + ) -> Result { + log::info!("Creating shell with {} windows", definitions.len()); + + if definitions.is_empty() { + return Err(Error::Domain(DomainError::Configuration { + message: "At least one shell window definition is required".to_string(), + })); + } + + let shell_configs: Vec = definitions + .iter() + .map(|def| { + let component_definition = compilation_result + .component(&def.component_name) + .ok_or_else(|| { + Error::Domain(DomainError::Configuration { + message: format!( + "Component '{}' not found in compilation result", + def.component_name + ), + }) + })?; + + let wayland_config = WaylandWindowConfig::from_domain_config( + component_definition, + Some(Rc::clone(&compilation_result)), + def.config.clone(), + ); + + Ok(ShellWindowConfig { + name: def.component_name.clone(), + config: wayland_config, + }) + }) + .collect::>>()?; + + let inner = layer_shika_adapters::WaylandWindowingSystem::new_multi(&shell_configs)?; + let facade = WindowingSystemFacade::new(inner); + let inner_rc = Rc::new(RefCell::new(facade)); + + let (sender, receiver) = channel::channel(); + + let mut windows = HashMap::new(); + for def in definitions { + windows.insert( + def.component_name.clone(), + ShellWindowHandle { + name: def.component_name.clone(), + }, + ); + } + + let shell = Self { + inner: Rc::clone(&inner_rc), + windows, + compilation_result, + popup_command_sender: sender, + }; + + shell.setup_popup_command_handler(receiver)?; + + log::info!( + "Shell created with windows: {:?}", + shell.shell_window_names() + ); + + Ok(shell) + } + + pub(crate) fn new_auto_discover( + compilation_result: Rc, + component_names: &[String], + ) -> Result { + log::info!( + "Creating shell with auto-discovery for {} components", + component_names.len() + ); + + if component_names.is_empty() { + return Err(Error::Domain(DomainError::Configuration { + message: "At least one component name is required for auto-discovery".to_string(), + })); + } + + let default_config = WindowConfig::default(); + + let shell_configs: Vec = component_names + .iter() + .map(|name| { + let component_definition = compilation_result.component(name).ok_or_else(|| { + Error::Domain(DomainError::Configuration { + message: format!("Component '{}' not found in compilation result", name), + }) + })?; + + let wayland_config = WaylandWindowConfig::from_domain_config( + component_definition, + Some(Rc::clone(&compilation_result)), + default_config.clone(), + ); + + Ok(ShellWindowConfig { + name: name.clone(), + config: wayland_config, + }) + }) + .collect::>>()?; + + let inner = layer_shika_adapters::WaylandWindowingSystem::new_multi(&shell_configs)?; + let facade = WindowingSystemFacade::new(inner); + let inner_rc = Rc::new(RefCell::new(facade)); + + let (sender, receiver) = channel::channel(); + + let mut windows = HashMap::new(); + for name in component_names { + windows.insert(name.clone(), ShellWindowHandle { name: name.clone() }); + } + + let shell = Self { + inner: Rc::clone(&inner_rc), + windows, + compilation_result, + popup_command_sender: sender, + }; + + shell.setup_popup_command_handler(receiver)?; + + log::info!( + "Shell created with auto-discovered windows: {:?}", + shell.shell_window_names() + ); + + Ok(shell) + } + + pub fn apply_window_config(&self, handler: &H) { + log::info!("Applying window configuration via handler"); + + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + + for window in system.app_state().all_outputs() { + let instance = window.component_instance(); + let surface_handle = LayerSurfaceHandle { + window_state: window, + }; + handler.configure_window(instance, surface_handle); + } + } + + pub fn apply_window_config_fn(&self, f: F) + where + F: Fn(&ComponentInstance, LayerSurfaceHandle<'_>), + { + self.apply_window_config(&f); + } + + fn setup_popup_command_handler(&self, receiver: channel::Channel) -> Result<()> { + let loop_handle = self.inner.borrow().inner_ref().event_loop_handle(); + let control = self.control(); + + loop_handle + .insert_source(receiver, move |event, (), app_state| { + if let channel::Event::Msg(command) = event { + let mut ctx = EventContext::from_app_state(app_state); + + match command { + PopupCommand::Show(request) => { + if let Err(e) = ctx.show_popup(&request, Some(control.clone())) { + log::error!("Failed to show popup: {}", e); + } + } + PopupCommand::Close(handle) => { + if let Err(e) = ctx.close_popup(handle) { + log::error!("Failed to close popup: {}", e); + } + } + PopupCommand::Resize { + handle, + width, + height, + } => { + if let Err(e) = ctx.resize_popup(handle, width, height) { + log::error!("Failed to resize popup: {}", e); + } + } + } + } + }) + .map_err(|e| { + Error::Adapter( + EventLoopError::InsertSource { + message: format!("Failed to setup popup command handler: {e:?}"), + } + .into(), + ) + })?; + + Ok(()) + } + + #[must_use] + pub fn control(&self) -> ShellControl { + ShellControl::new(self.popup_command_sender.clone()) + } + + pub fn shell_window(&self, name: &str) -> Option<&ShellWindowHandle> { + self.windows.get(name) + } + + pub fn shell_window_names(&self) -> Vec<&str> { + self.windows.keys().map(String::as_str).collect() + } + + pub fn event_loop_handle(&self) -> ShellEventLoopHandle { + ShellEventLoopHandle { + system: Rc::downgrade(&self.inner), + } + } + + pub fn run(&mut self) -> Result<()> { + log::info!( + "Starting shell event loop with {} windows", + self.windows.len() + ); + self.inner.borrow_mut().run()?; + Ok(()) + } + + pub fn with_component(&self, shell_window_name: &str, mut f: F) + where + F: FnMut(&ComponentInstance), + { + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + + if self.windows.contains_key(shell_window_name) { + for window in system.app_state().windows_by_shell_name(shell_window_name) { + f(window.component_instance()); + } + } + } + + pub fn with_all_components(&self, mut f: F) + where + F: FnMut(&str, &ComponentInstance), + { + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + + for name in self.windows.keys() { + if let Some(window) = system.app_state().primary_output() { + f(name, window.component_instance()); + } + } + } + + #[must_use] + pub fn compilation_result(&self) -> &Rc { + &self.compilation_result + } +} + +pub struct ShellEventLoopHandle { + system: Weak>, +} + +impl ShellEventLoopHandle { + pub fn insert_source( + &self, + source: S, + mut callback: F, + ) -> StdResult + where + S: EventSource + 'static, + F: FnMut(S::Event, &mut S::Metadata, ShellEventContext<'_>) -> R + 'static, + { + let system = self.system.upgrade().ok_or(Error::SystemDropped)?; + let loop_handle = system.borrow().inner_ref().event_loop_handle(); + + loop_handle + .insert_source(source, move |event, metadata, app_state| { + let ctx = ShellEventContext { app_state }; + callback(event, metadata, ctx) + }) + .map_err(|e| { + Error::Adapter( + EventLoopError::InsertSource { + message: format!("{e:?}"), + } + .into(), + ) + }) + } + + pub fn add_timer(&self, duration: Duration, mut callback: F) -> Result + where + F: FnMut(Instant, ShellEventContext<'_>) -> TimeoutAction + 'static, + { + let timer = Timer::from_duration(duration); + self.insert_source(timer, move |deadline, (), ctx| callback(deadline, ctx)) + } + + pub fn add_channel( + &self, + mut callback: F, + ) -> Result<(RegistrationToken, channel::Sender)> + where + T: 'static, + F: FnMut(T, ShellEventContext<'_>) + 'static, + { + let (sender, receiver) = channel::channel(); + let token = self.insert_source(receiver, move |event, (), ctx| { + if let channel::Event::Msg(msg) = event { + callback(msg, ctx); + } + })?; + Ok((token, sender)) + } + + pub fn add_fd( + &self, + fd: T, + interest: Interest, + mode: Mode, + mut callback: F, + ) -> Result + where + T: AsFd + 'static, + F: FnMut(ShellEventContext<'_>) + 'static, + { + let generic = Generic::new(fd, interest, mode); + self.insert_source(generic, move |_readiness, _fd, ctx| { + callback(ctx); + Ok(PostAction::Continue) + }) + } +} + +pub struct ShellEventContext<'a> { + app_state: &'a mut AppState, +} + +impl ShellEventContext<'_> { + pub fn get_shell_window_component( + &self, + _shell_window_name: &str, + ) -> Option<&ComponentInstance> { + self.app_state + .primary_output() + .map(WindowState::component_instance) + } + + pub fn get_shell_window_component_mut( + &mut self, + _shell_window_name: &str, + ) -> Option<&ComponentInstance> { + self.app_state + .primary_output() + .map(WindowState::component_instance) + } + + pub fn all_shell_window_components(&self) -> impl Iterator { + self.app_state + .all_outputs() + .map(WindowState::component_instance) + } + + pub fn render_frame_if_dirty(&mut self) -> Result<()> { + for window in self.app_state.all_outputs() { + window.render_frame_if_dirty()?; + } + Ok(()) + } +} diff --git a/crates/composition/src/shell_composition.rs b/crates/composition/src/shell_composition.rs new file mode 100644 index 0000000..4088c98 --- /dev/null +++ b/crates/composition/src/shell_composition.rs @@ -0,0 +1,83 @@ +use crate::shell::Shell; +use crate::{Error, Result}; +use layer_shika_adapters::platform::slint_interpreter::CompilationResult; +use layer_shika_domain::config::WindowConfig; +use layer_shika_domain::errors::DomainError; +use std::rc::Rc; + +#[derive(Debug, Clone)] +pub struct ShellWindowDefinition { + pub component_name: String, + pub config: WindowConfig, +} + +#[must_use] +pub struct ShellComposition { + compilation_result: Option>, + shell_windows: Vec, + auto_discover_components: Vec, +} + +impl ShellComposition { + pub fn new() -> Self { + Self { + compilation_result: None, + shell_windows: Vec::new(), + auto_discover_components: Vec::new(), + } + } + + pub fn with_compilation_result(mut self, result: Rc) -> Self { + self.compilation_result = Some(result); + self + } + + pub fn register_shell_window( + mut self, + component_name: impl Into, + config: WindowConfig, + ) -> Self { + self.shell_windows.push(ShellWindowDefinition { + component_name: component_name.into(), + config, + }); + self + } + + pub fn register_shell_windows(mut self, definitions: Vec) -> Self { + self.shell_windows.extend(definitions); + self + } + + pub fn auto_discover(mut self, component_names: Vec>) -> Self { + self.auto_discover_components = component_names.into_iter().map(Into::into).collect(); + self + } + + pub fn build(self) -> Result { + let compilation_result = self.compilation_result.ok_or_else(|| { + Error::Domain(DomainError::Configuration { + message: "No compilation result provided. Use with_compilation_result()" + .to_string(), + }) + })?; + + if !self.auto_discover_components.is_empty() { + return Shell::new_auto_discover(compilation_result, &self.auto_discover_components); + } + + if self.shell_windows.is_empty() { + return Err(Error::Domain(DomainError::Configuration { + message: "No shell windows registered. Use register_shell_window(), register_shell_windows(), or auto_discover()".to_string(), + })); + } + + Shell::new(compilation_result, &self.shell_windows) + } +} + +impl Default for ShellComposition { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/composition/src/system.rs b/crates/composition/src/system.rs index 43e7cc7..9932fcc 100644 --- a/crates/composition/src/system.rs +++ b/crates/composition/src/system.rs @@ -46,6 +46,10 @@ pub struct ShellControl { } impl ShellControl { + pub fn new(sender: channel::Sender) -> Self { + Self { sender } + } + pub fn show_popup(&self, request: &PopupRequest) -> Result<()> { self.sender .send(PopupCommand::Show(request.clone())) @@ -198,6 +202,12 @@ pub struct EventContext<'a> { app_state: &'a mut AppState, } +impl<'a> EventContext<'a> { + pub fn from_app_state(app_state: &'a mut AppState) -> Self { + Self { app_state } + } +} + fn extract_dimensions_from_callback(args: &[Value]) -> PopupDimensions { let defaults = PopupDimensions::default(); PopupDimensions::new( @@ -289,15 +299,21 @@ impl EventContext<'_> { req: &PopupRequest, resize_control: Option, ) -> Result { + log::info!("show_popup called for component '{}'", req.component); + let compilation_result = self.compilation_result().ok_or_else(|| { + log::error!("No compilation result available"); Error::Domain(DomainError::Configuration { message: "No compilation result available for popup creation".to_string(), }) })?; + log::debug!("Got compilation result, looking for component '{}'", req.component); + let definition = compilation_result .component(&req.component) .ok_or_else(|| { + log::error!("Component '{}' not found in compilation result", req.component); Error::Domain(DomainError::Configuration { message: format!( "{} component not found in compilation result", @@ -306,10 +322,13 @@ impl EventContext<'_> { }) })?; + log::debug!("Found component definition for '{}'", req.component); + self.close_current_popup()?; let is_using_active = self.app_state.active_output().is_some(); let active_window = self.active_or_primary_output().ok_or_else(|| { + log::error!("No active or primary output available"); Error::Domain(DomainError::Configuration { message: "No active or primary output available".to_string(), }) @@ -356,10 +375,6 @@ impl EventContext<'_> { popup_key_cell.set(popup_handle.key()); if let Some(popup_window) = popup_manager.get_popup_window(popup_handle.key()) { - if matches!(req.size, PopupSize::Content) { - log::debug!("Marking content-sized popup as repositioning from creation"); - popup_window.begin_repositioning(); - } popup_window.set_component_instance(instance); } else { return Err(Error::Domain(DomainError::Configuration { @@ -425,6 +440,7 @@ impl EventContext<'_> { let logical_height = height as i32; popup_manager.update_popup_viewport(handle.key(), logical_width, logical_height); + popup_manager.commit_popup_surface(handle.key()); log::debug!( "Updated popup viewport to logical size: {}x{} (from resize to {}x{})", logical_width, diff --git a/crates/domain/src/config.rs b/crates/domain/src/config.rs index 0acbe04..e3aec3b 100644 --- a/crates/domain/src/config.rs +++ b/crates/domain/src/config.rs @@ -1,6 +1,6 @@ use crate::dimensions::ScaleFactor; use crate::value_objects::anchor::AnchorEdges; -use crate::value_objects::dimensions::WindowHeight; +use crate::value_objects::dimensions::WindowDimension; use crate::value_objects::keyboard_interactivity::KeyboardInteractivity; use crate::value_objects::layer::Layer; use crate::value_objects::margins::Margins; @@ -8,7 +8,7 @@ use crate::value_objects::output_policy::OutputPolicy; #[derive(Debug, Clone)] pub struct WindowConfig { - pub height: WindowHeight, + pub dimensions: WindowDimension, pub margin: Margins, pub exclusive_zone: i32, pub scale_factor: ScaleFactor, @@ -23,7 +23,7 @@ impl WindowConfig { #[must_use] pub fn new() -> Self { Self { - height: WindowHeight::default(), + dimensions: WindowDimension::default(), margin: Margins::default(), exclusive_zone: -1, namespace: "layer-shika".to_owned(), diff --git a/crates/domain/src/prelude.rs b/crates/domain/src/prelude.rs index e5dfb3e..75a5173 100644 --- a/crates/domain/src/prelude.rs +++ b/crates/domain/src/prelude.rs @@ -9,7 +9,7 @@ pub use crate::errors::{DomainError, Result}; pub use crate::surface_dimensions::SurfaceDimensions; pub use crate::value_objects::anchor::AnchorEdges; pub use crate::value_objects::anchor_strategy::AnchorStrategy; -pub use crate::value_objects::dimensions::{PopupDimensions, WindowHeight}; +pub use crate::value_objects::dimensions::{PopupDimensions, WindowDimension}; pub use crate::value_objects::keyboard_interactivity::KeyboardInteractivity; pub use crate::value_objects::layer::Layer; pub use crate::value_objects::margins::Margins; diff --git a/crates/domain/src/value_objects/anchor.rs b/crates/domain/src/value_objects/anchor.rs index cf27cd2..f9fdef7 100644 --- a/crates/domain/src/value_objects/anchor.rs +++ b/crates/domain/src/value_objects/anchor.rs @@ -32,6 +32,16 @@ impl AnchorEdges { Self(Self::BOTTOM | Self::LEFT | Self::RIGHT) } + #[must_use] + pub const fn left_bar() -> Self { + Self(Self::LEFT | Self::TOP | Self::BOTTOM) + } + + #[must_use] + pub const fn right_bar() -> Self { + Self(Self::RIGHT | Self::TOP | Self::BOTTOM) + } + #[must_use] pub const fn with_top(mut self) -> Self { self.0 |= Self::TOP; diff --git a/crates/domain/src/value_objects/dimensions.rs b/crates/domain/src/value_objects/dimensions.rs index aeafd90..c2a8428 100644 --- a/crates/domain/src/value_objects/dimensions.rs +++ b/crates/domain/src/value_objects/dimensions.rs @@ -1,34 +1,44 @@ - #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct WindowHeight(u32); +pub struct WindowDimension { + width: u32, + height: u32, +} -impl WindowHeight { - pub fn new(height: u32) -> Self { - if height == 0 { - Self::default() - } else { - Self(height) +impl WindowDimension { + pub fn new(width: u32, height: u32) -> Self { + Self { + width: if width == 0 { + Self::default().width + } else { + width + }, + height: if height == 0 { + Self::default().height + } else { + height + }, } } - pub const fn from_raw(height: u32) -> Self { - Self(height) + pub const fn from_raw(width: u32, height: u32) -> Self { + Self { width, height } } - pub const fn value(&self) -> u32 { - self.0 + pub const fn width(&self) -> u32 { + self.width + } + + pub const fn height(&self) -> u32 { + self.height } } -impl Default for WindowHeight { +impl Default for WindowDimension { fn default() -> Self { - Self(30) - } -} - -impl From for WindowHeight { - fn from(height: u32) -> Self { - Self::new(height) + Self { + width: 20, + height: 20, + } } } diff --git a/src/lib.rs b/src/lib.rs index d6cd843..28ab3ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,6 +30,27 @@ //! # Ok::<(), layer_shika::Error>(()) //! ``` //! +//! # Multi-Window Shell +//! +//! For multi-window shell applications: +//! +//! ```rust,no_run +//! use layer_shika::prelude::*; +//! use std::rc::Rc; +//! +//! // Load Slint file with multiple shell window components +//! let compilation_result = Rc::new(/* ... */); +//! +//! // Create shell with typed WindowConfig +//! let shell = ShellComposition::new() +//! .with_compilation_result(compilation_result) +//! .register_shell_window("TopBar", WindowConfig::default()) +//! .build()?; +//! +//! shell.run()?; +//! # Ok::<(), layer_shika::Error>(()) +//! ``` +//! //! # Re-exports //! //! This crate re-exports commonly needed types from its dependencies: @@ -48,6 +69,11 @@ pub use layer_shika_composition::{ Result, ShellControl, }; +pub use layer_shika_composition::{ + LayerSurfaceHandle, Shell, ShellComposition, ShellEventContext, ShellEventLoopHandle, + ShellWindowConfigHandler, ShellWindowDefinition, ShellWindowHandle, +}; + pub use layer_shika_composition::{slint, slint_interpreter}; /// Re-exported calloop types for event loop integration diff --git a/src/prelude.rs b/src/prelude.rs index ae97841..6fa7b77 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -8,20 +8,25 @@ #![allow(clippy::pub_use)] -// Core API types pub use crate::{ App, Error, EventContext, EventLoopHandle, LayerShika, PopupWindow, Result, ShellControl, }; -// Domain value objects +pub use crate::{ + LayerSurfaceHandle, Shell, ShellComposition, ShellEventContext, ShellEventLoopHandle, + ShellWindowConfigHandler, ShellWindowDefinition, ShellWindowHandle, +}; + pub use crate::{ AnchorEdges, KeyboardInteractivity, Layer, OutputGeometry, OutputHandle, OutputInfo, OutputPolicy, OutputRegistry, PopupHandle, PopupPlacement, PopupPositioningMode, PopupRequest, PopupSize, }; -// Event loop types +pub use layer_shika_composition::prelude::{ + Anchor, Margins, ScaleFactor, WindowConfig, WindowDimension, +}; + pub use crate::calloop; -// UI framework re-exports pub use crate::{slint, slint_interpreter};