From c2064c601222d06a25cfd093edf4ee782d0a2895 Mon Sep 17 00:00:00 2001 From: drendog Date: Sun, 7 Dec 2025 21:01:24 +0100 Subject: [PATCH] feat: add surface command for shell control --- .../src/wayland/surfaces/app_state.rs | 10 + .../src/wayland/surfaces/surface_state.rs | 36 +++ crates/composition/src/layer_surface.rs | 25 ++ crates/composition/src/lib.rs | 7 +- crates/composition/src/shell.rs | 167 +++++++++-- crates/composition/src/system.rs | 271 +++++++++++++++++- 6 files changed, 480 insertions(+), 36 deletions(-) diff --git a/crates/adapters/src/wayland/surfaces/app_state.rs b/crates/adapters/src/wayland/surfaces/app_state.rs index 951b19b..8e1b3cd 100644 --- a/crates/adapters/src/wayland/surfaces/app_state.rs +++ b/crates/adapters/src/wayland/surfaces/app_state.rs @@ -376,6 +376,16 @@ impl AppState { .map(|(_, v)| v) } + pub fn surfaces_by_name_mut( + &mut self, + surface_name: &str, + ) -> impl Iterator { + self.surfaces + .iter_mut() + .filter(move |(k, _)| k.surface_name == surface_name) + .map(|(_, v)| v) + } + pub fn get_output_by_handle(&self, handle: OutputHandle) -> Option<&PerOutputSurface> { self.get_first_surface_for_output(handle) } diff --git a/crates/adapters/src/wayland/surfaces/surface_state.rs b/crates/adapters/src/wayland/surfaces/surface_state.rs index 402a28c..45747bd 100644 --- a/crates/adapters/src/wayland/surfaces/surface_state.rs +++ b/crates/adapters/src/wayland/surfaces/surface_state.rs @@ -123,6 +123,42 @@ impl SurfaceState { self.rendering.update_size(width, height, scale_factor); } + #[allow(clippy::cast_precision_loss)] + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_sign_loss)] + pub fn update_size_with_compositor_logic( + &mut self, + requested_width: u32, + requested_height: u32, + ) { + let scale_factor = self.event_context.borrow().scale_factor(); + let output_width = self.output_size().width; + + let target_width = if requested_width == 0 || (requested_width == 1 && output_width > 1) { + if scale_factor > 1.0 { + (output_width as f32 / scale_factor).round() as u32 + } else { + output_width + } + } else { + requested_width + }; + + let target_height = if requested_height > 0 { + requested_height + } else { + let h = self.height(); + if scale_factor > 1.0 { + (h as f32 / scale_factor).round() as u32 + } else { + h + } + }; + + self.rendering + .update_size(target_width, target_height, scale_factor); + } + #[allow(clippy::cast_possible_truncation)] pub fn set_current_pointer_position(&mut self, physical_x: f64, physical_y: f64) { self.event_context diff --git a/crates/composition/src/layer_surface.rs b/crates/composition/src/layer_surface.rs index 6e3df29..1a8b6f7 100644 --- a/crates/composition/src/layer_surface.rs +++ b/crates/composition/src/layer_surface.rs @@ -1,6 +1,7 @@ use layer_shika_adapters::SurfaceState; use layer_shika_adapters::platform::slint_interpreter::ComponentInstance; use layer_shika_adapters::platform::wayland::{Anchor, WaylandKeyboardInteractivity, WaylandLayer}; +use layer_shika_domain::value_objects::anchor::AnchorEdges; use layer_shika_domain::value_objects::keyboard_interactivity::KeyboardInteractivity; use layer_shika_domain::value_objects::layer::Layer; use layer_shika_domain::value_objects::margins::Margins; @@ -18,6 +19,30 @@ impl<'a> LayerSurfaceHandle<'a> { self.window_state.layer_surface().set_anchor(anchor); } + pub fn set_anchor_edges(&self, anchor: AnchorEdges) { + let wayland_anchor = Self::convert_anchor(anchor); + self.window_state.layer_surface().set_anchor(wayland_anchor); + } + + fn convert_anchor(anchor: AnchorEdges) -> Anchor { + let mut result = Anchor::empty(); + + if anchor.has_top() { + result = result.union(Anchor::Top); + } + if anchor.has_bottom() { + result = result.union(Anchor::Bottom); + } + if anchor.has_left() { + result = result.union(Anchor::Left); + } + if anchor.has_right() { + result = result.union(Anchor::Right); + } + + result + } + pub fn set_size(&self, width: u32, height: u32) { self.window_state.layer_surface().set_size(width, height); } diff --git a/crates/composition/src/lib.rs b/crates/composition/src/lib.rs index aac65db..df8ff46 100644 --- a/crates/composition/src/lib.rs +++ b/crates/composition/src/lib.rs @@ -33,7 +33,7 @@ pub use layer_shika_domain::value_objects::popup_request::{ pub use layer_surface::{LayerSurfaceHandle, ShellSurfaceConfigHandler}; pub use popup_builder::PopupBuilder; pub use shell_runtime::{DEFAULT_SURFACE_NAME, ShellRuntime}; -pub use system::{EventDispatchContext, ShellControl}; +pub use system::{EventDispatchContext, ShellControl, SurfaceControlHandle}; pub use value_conversion::IntoValue; pub use shell::{ @@ -70,8 +70,9 @@ pub mod prelude { OutputPolicy, OutputRegistry, PopupBuilder, PopupHandle, PopupPlacement, PopupPositioningMode, PopupRequest, PopupSize, PopupWindow, Result, Shell, ShellBuilder, ShellConfig, ShellControl, ShellEventContext, ShellEventLoop, ShellRuntime, - ShellSurfaceConfigHandler, SurfaceComponentConfig, SurfaceConfigBuilder, SurfaceDefinition, - SurfaceEntry, SurfaceHandle, SurfaceMetadata, SurfaceRegistry, + ShellSurfaceConfigHandler, SurfaceComponentConfig, SurfaceConfigBuilder, + SurfaceControlHandle, SurfaceDefinition, SurfaceEntry, SurfaceHandle, SurfaceMetadata, + SurfaceRegistry, }; pub use crate::calloop::{Generic, Interest, Mode, PostAction, RegistrationToken, Timer}; diff --git a/crates/composition/src/shell.rs b/crates/composition/src/shell.rs index 2ab0ee0..7bfbb85 100644 --- a/crates/composition/src/shell.rs +++ b/crates/composition/src/shell.rs @@ -4,7 +4,9 @@ use crate::popup_builder::PopupBuilder; use crate::shell_config::{CompiledUiSource, ShellConfig}; use crate::shell_runtime::ShellRuntime; use crate::surface_registry::{SurfaceDefinition, SurfaceEntry, SurfaceRegistry}; -use crate::system::{PopupCommand, ShellControl}; +use crate::system::{ + EventDispatchContext, PopupCommand, ShellCommand, ShellControl, SurfaceCommand, +}; use crate::value_conversion::IntoValue; use crate::{Error, Result}; use layer_shika_adapters::errors::EventLoopError; @@ -220,7 +222,7 @@ pub struct Shell { inner: Rc>, registry: SurfaceRegistry, compilation_result: Rc, - popup_command_sender: channel::Sender, + command_sender: channel::Sender, output_connected_handlers: Rc>>, output_disconnected_handlers: Rc>>, } @@ -403,12 +405,12 @@ impl Shell { inner: Rc::clone(&inner_rc), registry, compilation_result, - popup_command_sender: sender, + command_sender: sender, output_connected_handlers: Rc::new(RefCell::new(Vec::new())), output_disconnected_handlers: Rc::new(RefCell::new(Vec::new())), }; - shell.setup_popup_command_handler(receiver)?; + shell.setup_command_handler(receiver)?; log::info!("Shell created (single-window mode)"); @@ -463,12 +465,12 @@ impl Shell { inner: Rc::clone(&inner_rc), registry, compilation_result, - popup_command_sender: sender, + command_sender: sender, output_connected_handlers: Rc::new(RefCell::new(Vec::new())), output_disconnected_handlers: Rc::new(RefCell::new(Vec::new())), }; - shell.setup_popup_command_handler(receiver)?; + shell.setup_command_handler(receiver)?; log::info!( "Shell created (multi-surface mode) with surfaces: {:?}", @@ -478,7 +480,7 @@ impl Shell { Ok(shell) } - fn setup_popup_command_handler(&self, receiver: channel::Channel) -> Result<()> { + fn setup_command_handler(&self, receiver: channel::Channel) -> Result<()> { let loop_handle = self.inner.borrow().event_loop_handle(); let control = self.control(); @@ -488,23 +490,15 @@ impl Shell { let mut ctx = crate::system::EventDispatchContext::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); - } + ShellCommand::Popup(popup_cmd) => { + Self::handle_popup_command(popup_cmd, &mut ctx, &control); } - PopupCommand::Close(handle) => { - if let Err(e) = ctx.close_popup(handle) { - log::error!("Failed to close popup: {}", e); - } + ShellCommand::Surface(surface_cmd) => { + Self::handle_surface_command(surface_cmd, &mut ctx); } - PopupCommand::Resize { - handle, - width, - height, - } => { - if let Err(e) = ctx.resize_popup(handle, width, height) { - log::error!("Failed to resize popup: {}", e); + ShellCommand::Render => { + if let Err(e) = ctx.render_frame_if_dirty() { + log::error!("Failed to render frame: {}", e); } } } @@ -513,7 +507,7 @@ impl Shell { .map_err(|e| { Error::Adapter( EventLoopError::InsertSource { - message: format!("Failed to setup popup command handler: {e:?}"), + message: format!("Failed to setup command handler: {e:?}"), } .into(), ) @@ -522,9 +516,134 @@ impl Shell { Ok(()) } + fn handle_popup_command( + command: PopupCommand, + ctx: &mut EventDispatchContext<'_>, + control: &ShellControl, + ) { + 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); + } + } + } + } + + fn handle_surface_command(command: SurfaceCommand, ctx: &mut EventDispatchContext<'_>) { + match command { + SurfaceCommand::Resize { + name, + width, + height, + } => { + log::debug!("Surface command: Resize '{}' to {}x{}", name, width, height); + for surface in ctx.surfaces_by_name_mut(&name) { + let handle = LayerSurfaceHandle::from_window_state(surface); + handle.set_size(width, height); + handle.commit(); + + surface.update_size_with_compositor_logic(width, height); + } + } + SurfaceCommand::SetAnchor { name, anchor } => { + log::debug!("Surface command: SetAnchor '{}' to {:?}", name, anchor); + for surface in ctx.surfaces_by_name(&name) { + let handle = LayerSurfaceHandle::from_window_state(surface); + handle.set_anchor_edges(anchor); + handle.commit(); + } + } + SurfaceCommand::SetExclusiveZone { name, zone } => { + log::debug!("Surface command: SetExclusiveZone '{}' to {}", name, zone); + for surface in ctx.surfaces_by_name(&name) { + let handle = LayerSurfaceHandle::from_window_state(surface); + handle.set_exclusive_zone(zone); + handle.commit(); + } + } + SurfaceCommand::SetMargins { name, margins } => { + log::debug!("Surface command: SetMargins '{}' to {:?}", name, margins); + for surface in ctx.surfaces_by_name(&name) { + let handle = LayerSurfaceHandle::from_window_state(surface); + handle.set_margins(margins); + handle.commit(); + } + } + SurfaceCommand::SetLayer { name, layer } => { + log::debug!("Surface command: SetLayer '{}' to {:?}", name, layer); + for surface in ctx.surfaces_by_name(&name) { + let handle = LayerSurfaceHandle::from_window_state(surface); + handle.set_layer(layer); + handle.commit(); + } + } + SurfaceCommand::SetKeyboardInteractivity { name, mode } => { + log::debug!( + "Surface command: SetKeyboardInteractivity '{}' to {:?}", + name, + mode + ); + for surface in ctx.surfaces_by_name(&name) { + let handle = LayerSurfaceHandle::from_window_state(surface); + handle.set_keyboard_interactivity(mode); + handle.commit(); + } + } + SurfaceCommand::SetOutputPolicy { name, policy } => { + log::debug!( + "Surface command: SetOutputPolicy '{}' to {:?}", + name, + policy + ); + log::warn!( + "SetOutputPolicy is not yet implemented - requires runtime surface spawning" + ); + } + SurfaceCommand::SetScaleFactor { name, factor } => { + log::debug!("Surface command: SetScaleFactor '{}' to {:?}", name, factor); + log::warn!( + "SetScaleFactor is not yet implemented - requires runtime surface property updates" + ); + } + SurfaceCommand::ApplyConfig { name, config } => { + log::debug!("Surface command: ApplyConfig '{}'", name); + for surface in ctx.surfaces_by_name(&name) { + let handle = LayerSurfaceHandle::from_window_state(surface); + + handle.set_size(config.dimensions.width(), config.dimensions.height()); + handle.set_anchor_edges(config.anchor); + handle.set_exclusive_zone(config.exclusive_zone); + handle.set_margins(config.margin); + handle.set_layer(config.layer); + handle.set_keyboard_interactivity(config.keyboard_interactivity); + handle.commit(); + } + } + } + + if let Err(e) = ctx.render_frame_if_dirty() { + log::error!("Failed to render frame after surface command: {}", e); + } + } + #[must_use] pub fn control(&self) -> ShellControl { - ShellControl::new(self.popup_command_sender.clone()) + ShellControl::new(self.command_sender.clone()) } pub fn surface_names(&self) -> Vec<&str> { diff --git a/crates/composition/src/system.rs b/crates/composition/src/system.rs index 188862b..2aef61e 100644 --- a/crates/composition/src/system.rs +++ b/crates/composition/src/system.rs @@ -1,4 +1,5 @@ use crate::event_loop::FromAppState; +use crate::layer_surface::LayerSurfaceHandle; use crate::{Error, Result}; use layer_shika_adapters::platform::calloop::channel; use layer_shika_adapters::platform::slint::ComponentHandle; @@ -6,8 +7,12 @@ use layer_shika_adapters::platform::slint_interpreter::{ CompilationResult, ComponentDefinition, ComponentInstance, Value, }; use layer_shika_adapters::{AppState, PopupManager, SurfaceState}; +use layer_shika_domain::config::SurfaceConfig; use layer_shika_domain::entities::output_registry::OutputRegistry; use layer_shika_domain::errors::DomainError; +use layer_shika_domain::prelude::{ + AnchorEdges, KeyboardInteractivity, Layer, Margins, OutputPolicy, ScaleFactor, +}; use layer_shika_domain::value_objects::dimensions::PopupDimensions; use layer_shika_domain::value_objects::output_handle::OutputHandle; use layer_shika_domain::value_objects::output_info::OutputInfo; @@ -28,19 +33,65 @@ pub enum PopupCommand { }, } +pub enum SurfaceCommand { + Resize { + name: String, + width: u32, + height: u32, + }, + SetAnchor { + name: String, + anchor: AnchorEdges, + }, + SetExclusiveZone { + name: String, + zone: i32, + }, + SetMargins { + name: String, + margins: Margins, + }, + SetLayer { + name: String, + layer: Layer, + }, + SetOutputPolicy { + name: String, + policy: OutputPolicy, + }, + SetScaleFactor { + name: String, + factor: ScaleFactor, + }, + SetKeyboardInteractivity { + name: String, + mode: KeyboardInteractivity, + }, + ApplyConfig { + name: String, + config: SurfaceConfig, + }, +} + +pub enum ShellCommand { + Popup(PopupCommand), + Surface(SurfaceCommand), + Render, +} + #[derive(Clone)] pub struct ShellControl { - sender: channel::Sender, + sender: channel::Sender, } impl ShellControl { - pub fn new(sender: channel::Sender) -> Self { + pub fn new(sender: channel::Sender) -> Self { Self { sender } } pub fn show_popup(&self, request: &PopupRequest) -> Result<()> { self.sender - .send(PopupCommand::Show(request.clone())) + .send(ShellCommand::Popup(PopupCommand::Show(request.clone()))) .map_err(|_| { Error::Domain(DomainError::Configuration { message: "Failed to send popup show command: channel closed".to_string(), @@ -76,26 +127,186 @@ impl ShellControl { } pub fn close_popup(&self, handle: PopupHandle) -> Result<()> { - self.sender.send(PopupCommand::Close(handle)).map_err(|_| { - Error::Domain(DomainError::Configuration { - message: "Failed to send popup close command: channel closed".to_string(), + self.sender + .send(ShellCommand::Popup(PopupCommand::Close(handle))) + .map_err(|_| { + Error::Domain(DomainError::Configuration { + message: "Failed to send popup close command: channel closed".to_string(), + }) }) - }) } pub fn resize_popup(&self, handle: PopupHandle, width: f32, height: f32) -> Result<()> { self.sender - .send(PopupCommand::Resize { + .send(ShellCommand::Popup(PopupCommand::Resize { handle, width, height, - }) + })) .map_err(|_| { Error::Domain(DomainError::Configuration { message: "Failed to send popup resize command: channel closed".to_string(), }) }) } + + pub fn request_redraw(&self) -> Result<()> { + self.sender.send(ShellCommand::Render).map_err(|_| { + Error::Domain(DomainError::Configuration { + message: "Failed to send redraw command: channel closed".to_string(), + }) + }) + } + + pub fn surface(&self, name: impl Into) -> SurfaceControlHandle { + SurfaceControlHandle { + name: name.into(), + sender: self.sender.clone(), + } + } +} + +pub struct SurfaceControlHandle { + name: String, + sender: channel::Sender, +} + +impl SurfaceControlHandle { + pub fn resize(&self, width: u32, height: u32) -> Result<()> { + self.sender + .send(ShellCommand::Surface(SurfaceCommand::Resize { + name: self.name.clone(), + width, + height, + })) + .map_err(|_| { + Error::Domain(DomainError::Configuration { + message: "Failed to send surface resize command: channel closed".to_string(), + }) + }) + } + + pub fn set_width(&self, width: u32) -> Result<()> { + self.resize(width, 0) + } + + pub fn set_height(&self, height: u32) -> Result<()> { + self.resize(0, height) + } + + pub fn set_anchor(&self, anchor: AnchorEdges) -> Result<()> { + self.sender + .send(ShellCommand::Surface(SurfaceCommand::SetAnchor { + name: self.name.clone(), + anchor, + })) + .map_err(|_| { + Error::Domain(DomainError::Configuration { + message: "Failed to send surface set_anchor command: channel closed" + .to_string(), + }) + }) + } + + pub fn set_exclusive_zone(&self, zone: i32) -> Result<()> { + self.sender + .send(ShellCommand::Surface(SurfaceCommand::SetExclusiveZone { + name: self.name.clone(), + zone, + })) + .map_err(|_| { + Error::Domain(DomainError::Configuration { + message: "Failed to send surface set_exclusive_zone command: channel closed" + .to_string(), + }) + }) + } + + pub fn set_margins(&self, margins: impl Into) -> Result<()> { + self.sender + .send(ShellCommand::Surface(SurfaceCommand::SetMargins { + name: self.name.clone(), + margins: margins.into(), + })) + .map_err(|_| { + Error::Domain(DomainError::Configuration { + message: "Failed to send surface set_margins command: channel closed" + .to_string(), + }) + }) + } + + pub fn set_layer(&self, layer: Layer) -> Result<()> { + self.sender + .send(ShellCommand::Surface(SurfaceCommand::SetLayer { + name: self.name.clone(), + layer, + })) + .map_err(|_| { + Error::Domain(DomainError::Configuration { + message: "Failed to send surface set_layer command: channel closed".to_string(), + }) + }) + } + + pub fn set_output_policy(&self, policy: OutputPolicy) -> Result<()> { + self.sender + .send(ShellCommand::Surface(SurfaceCommand::SetOutputPolicy { + name: self.name.clone(), + policy, + })) + .map_err(|_| { + Error::Domain(DomainError::Configuration { + message: "Failed to send surface set_output_policy command: channel closed" + .to_string(), + }) + }) + } + + pub fn set_scale_factor(&self, factor: ScaleFactor) -> Result<()> { + self.sender + .send(ShellCommand::Surface(SurfaceCommand::SetScaleFactor { + name: self.name.clone(), + factor, + })) + .map_err(|_| { + Error::Domain(DomainError::Configuration { + message: "Failed to send surface set_scale_factor command: channel closed" + .to_string(), + }) + }) + } + + pub fn set_keyboard_interactivity(&self, mode: KeyboardInteractivity) -> Result<()> { + self.sender + .send(ShellCommand::Surface( + SurfaceCommand::SetKeyboardInteractivity { + name: self.name.clone(), + mode, + }, + )) + .map_err(|_| { + Error::Domain(DomainError::Configuration { + message: + "Failed to send surface set_keyboard_interactivity command: channel closed" + .to_string(), + }) + }) + } + + pub fn apply_config(&self, config: SurfaceConfig) -> Result<()> { + self.sender + .send(ShellCommand::Surface(SurfaceCommand::ApplyConfig { + name: self.name.clone(), + config, + })) + .map_err(|_| { + Error::Domain(DomainError::Configuration { + message: "Failed to send surface apply_config command: channel closed" + .to_string(), + }) + }) + } } pub struct EventDispatchContext<'a> { @@ -121,6 +332,17 @@ fn extract_dimensions_from_callback(args: &[Value]) -> PopupDimensions { } impl EventDispatchContext<'_> { + pub(crate) fn surfaces_by_name(&self, name: &str) -> impl Iterator { + self.app_state.surfaces_by_name(name) + } + + pub(crate) fn surfaces_by_name_mut( + &mut self, + name: &str, + ) -> impl Iterator { + self.app_state.surfaces_by_name_mut(name) + } + pub fn with_surface(&self, name: &str, f: F) -> Result where F: FnOnce(&ComponentInstance) -> R, @@ -572,4 +794,35 @@ impl EventDispatchContext<'_> { }) }) } + + pub fn configure_surface(&mut self, name: &str, f: F) -> Result<()> + where + F: FnOnce(&ComponentInstance, LayerSurfaceHandle<'_>), + { + let surface = self + .app_state + .surfaces_by_name(name) + .next() + .ok_or_else(|| { + Error::Domain(DomainError::Configuration { + message: format!("Surface '{}' not found", name), + }) + })?; + + let handle = LayerSurfaceHandle::from_window_state(surface); + let component = surface.component_instance(); + f(component, handle); + Ok(()) + } + + pub fn configure_all_surfaces(&mut self, mut f: F) + where + F: FnMut(&ComponentInstance, LayerSurfaceHandle<'_>), + { + for surface in self.app_state.all_outputs() { + let handle = LayerSurfaceHandle::from_window_state(surface); + let component = surface.component_instance(); + f(component, handle); + } + } }