diff --git a/crates/composition/src/builder.rs b/crates/composition/src/builder.rs deleted file mode 100644 index 1c1c83c..0000000 --- a/crates/composition/src/builder.rs +++ /dev/null @@ -1,228 +0,0 @@ -use crate::Result; -use crate::system::SingleWindowShell; -use layer_shika_adapters::platform::slint_interpreter::{CompilationResult, Compiler}; -use layer_shika_domain::errors::DomainError; -use layer_shika_domain::prelude::{ - AnchorEdges, KeyboardInteractivity, Layer, Margins, OutputPolicy, ScaleFactor, WindowConfig, - WindowDimension, -}; -use spin_on::spin_on; -use std::path::{Path, PathBuf}; -use std::rc::Rc; - -pub struct NeedsComponent; -pub struct HasComponent { - component_name: String, - compilation_result: Rc, -} - -pub struct LayerShika { - state: State, - config: WindowConfig, -} - -impl LayerShika { - #[must_use] - pub fn new( - compilation_result: Rc, - component_name: impl Into, - ) -> LayerShika { - LayerShika { - state: HasComponent { - component_name: component_name.into(), - compilation_result, - }, - config: WindowConfig::default(), - } - } - - pub fn from_file(path: impl AsRef) -> Result> { - Self::from_file_with_component(path, "Main") - } - - pub fn from_file_with_component( - path: impl AsRef, - component_name: impl AsRef, - ) -> Result> { - Self::from_file_with_compiler(path, &mut Compiler::default(), component_name.as_ref()) - } - - pub fn from_file_with_compiler( - path: impl AsRef, - compiler: &mut Compiler, - component_name: &str, - ) -> Result> { - let compilation_result = spin_on(compiler.build_from_path(path.as_ref())); - let diagnostics: Vec<_> = compilation_result.diagnostics().collect(); - if !diagnostics.is_empty() { - let messages: Vec = diagnostics.iter().map(ToString::to_string).collect(); - return Err(DomainError::Configuration { - message: format!( - "Failed to compile Slint file '{}':\n{}", - path.as_ref().display(), - messages.join("\n") - ), - } - .into()); - } - - compilation_result - .component(component_name) - .ok_or_else(|| DomainError::Configuration { - message: format!( - "Component '{}' not found in Slint file '{}'", - component_name, - path.as_ref().display() - ), - })?; - - Ok(LayerShika { - state: HasComponent { - component_name: component_name.to_string(), - compilation_result: Rc::new(compilation_result), - }, - config: WindowConfig::default(), - }) - } - - pub fn from_source(source: impl AsRef) -> Result> { - Self::from_source_with_component(source, "Main") - } - - pub fn from_source_with_component( - source: impl AsRef, - component_name: impl AsRef, - ) -> Result> { - Self::from_source_with_compiler(source, &mut Compiler::default(), component_name.as_ref()) - } - - pub fn from_source_with_compiler( - source: impl AsRef, - compiler: &mut Compiler, - component_name: &str, - ) -> Result> { - let compilation_result = - spin_on(compiler.build_from_source(source.as_ref().to_string(), PathBuf::default())); - - let diagnostics: Vec<_> = compilation_result.diagnostics().collect(); - if !diagnostics.is_empty() { - let messages: Vec = diagnostics.iter().map(ToString::to_string).collect(); - return Err(DomainError::Configuration { - message: format!( - "Failed to compile Slint source code:\n{}", - messages.join("\n") - ), - } - .into()); - } - - compilation_result - .component(component_name) - .ok_or_else(|| DomainError::Configuration { - message: format!( - "Component '{}' not found in Slint source code", - component_name - ), - })?; - - Ok(LayerShika { - state: HasComponent { - component_name: component_name.to_string(), - compilation_result: Rc::new(compilation_result), - }, - config: WindowConfig::default(), - }) - } -} - -impl LayerShika { - #[must_use] - pub fn size(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.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 - } - - #[must_use] - pub const fn layer(mut self, layer: Layer) -> Self { - self.config.layer = layer; - self - } - - #[must_use] - pub fn margin(mut self, margin: impl Into) -> Self { - self.config.margin = margin.into(); - self - } - - #[must_use] - pub const fn anchor(mut self, anchor: AnchorEdges) -> Self { - self.config.anchor = anchor; - self - } - - #[must_use] - pub const fn exclusive_zone(mut self, zone: i32) -> Self { - self.config.exclusive_zone = zone; - self - } - - #[must_use] - pub fn namespace(mut self, namespace: impl Into) -> Self { - self.config.namespace = namespace.into(); - self - } - - #[must_use] - pub fn scale_factor(mut self, sf: impl TryInto) -> Self { - self.config.scale_factor = sf.try_into().unwrap_or_default(); - self - } - - #[must_use] - pub const fn keyboard_interactivity(mut self, mode: KeyboardInteractivity) -> Self { - self.config.keyboard_interactivity = mode; - self - } - - #[must_use] - pub fn output_policy(mut self, policy: OutputPolicy) -> Self { - self.config.output_policy = policy; - self - } - - pub fn build(self) -> Result { - let component_definition = self - .state - .compilation_result - .component(&self.state.component_name) - .ok_or_else(|| DomainError::Configuration { - message: format!( - "Component '{}' not found in compilation result", - self.state.component_name - ), - })?; - - SingleWindowShell::new( - component_definition, - Some(self.state.compilation_result), - self.config, - ) - } - - pub fn run(self) -> Result<()> { - let mut app = self.build()?; - app.run() - } -} diff --git a/crates/composition/src/layer_shika.rs b/crates/composition/src/layer_shika.rs new file mode 100644 index 0000000..5f72037 --- /dev/null +++ b/crates/composition/src/layer_shika.rs @@ -0,0 +1,897 @@ +use crate::event_loop::{EventLoopHandleBase, FromAppState}; +use crate::popup_builder::PopupBuilder; +use crate::shell::LayerSurfaceHandle; +use crate::shell_runtime::ShellRuntime; +use crate::system::{PopupCommand, ShellControl}; +use crate::value_conversion::IntoValue; +use crate::{Error, Result}; +use layer_shika_adapters::errors::EventLoopError; +use layer_shika_adapters::platform::calloop::channel; +use layer_shika_adapters::platform::slint_interpreter::{ + CompilationResult, Compiler, ComponentInstance, Value, +}; +use layer_shika_adapters::{ + AppState, ShellWindowConfig, WaylandWindowConfig, WindowState, WindowingSystemFacade, +}; +use layer_shika_domain::config::WindowConfig; +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, WindowDimension, +}; +use layer_shika_domain::value_objects::output_handle::OutputHandle; +use layer_shika_domain::value_objects::output_info::OutputInfo; +use spin_on::spin_on; +use std::cell::RefCell; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::rc::Rc; + +pub const DEFAULT_COMPONENT_NAME: &str = "Main"; + +#[derive(Debug, Clone)] +pub struct WindowDefinition { + pub component: String, + pub config: WindowConfig, +} + +enum CompilationSource { + File { path: PathBuf, compiler: Compiler }, + Source { code: String, compiler: Compiler }, + Compiled(Rc), +} + +pub struct ShellBuilder { + compilation: CompilationSource, + windows: Vec, +} + +impl ShellBuilder { + pub fn window(self, component: impl Into) -> WindowConfigBuilder { + WindowConfigBuilder { + shell_builder: self, + component: component.into(), + config: WindowConfig::default(), + } + } + + #[must_use] + pub fn discover_windows( + mut self, + components: impl IntoIterator>, + ) -> Self { + for component in components { + self.windows.push(WindowDefinition { + component: component.into(), + config: WindowConfig::default(), + }); + } + self + } + + pub fn build(self) -> Result { + let windows = if self.windows.is_empty() { + vec![WindowDefinition { + component: DEFAULT_COMPONENT_NAME.to_string(), + config: WindowConfig::default(), + }] + } else { + self.windows + }; + + let compilation_result = match self.compilation { + CompilationSource::File { path, compiler } => { + let result = spin_on(compiler.build_from_path(&path)); + let diagnostics: Vec<_> = result.diagnostics().collect(); + if !diagnostics.is_empty() { + let messages: Vec = + diagnostics.iter().map(ToString::to_string).collect(); + return Err(DomainError::Configuration { + message: format!( + "Failed to compile Slint file '{}':\n{}", + path.display(), + messages.join("\n") + ), + } + .into()); + } + Rc::new(result) + } + CompilationSource::Source { code, compiler } => { + let result = spin_on(compiler.build_from_source(code, PathBuf::default())); + let diagnostics: Vec<_> = result.diagnostics().collect(); + if !diagnostics.is_empty() { + let messages: Vec = + diagnostics.iter().map(ToString::to_string).collect(); + return Err(DomainError::Configuration { + message: format!( + "Failed to compile Slint source:\n{}", + messages.join("\n") + ), + } + .into()); + } + Rc::new(result) + } + CompilationSource::Compiled(result) => result, + }; + + Runtime::new(compilation_result, windows) + } +} + +pub struct WindowConfigBuilder { + shell_builder: ShellBuilder, + component: String, + config: WindowConfig, +} + +impl WindowConfigBuilder { + #[must_use] + pub fn size(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.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 + } + + #[must_use] + pub const fn layer(mut self, layer: Layer) -> Self { + self.config.layer = layer; + self + } + + #[must_use] + pub fn margin(mut self, margin: impl Into) -> Self { + self.config.margin = margin.into(); + self + } + + #[must_use] + pub const fn anchor(mut self, anchor: AnchorEdges) -> Self { + self.config.anchor = anchor; + self + } + + #[must_use] + pub const fn exclusive_zone(mut self, zone: i32) -> Self { + self.config.exclusive_zone = zone; + self + } + + #[must_use] + pub fn namespace(mut self, namespace: impl Into) -> Self { + self.config.namespace = namespace.into(); + self + } + + #[must_use] + pub fn scale_factor(mut self, sf: impl TryInto) -> Self { + self.config.scale_factor = sf.try_into().unwrap_or_default(); + self + } + + #[must_use] + pub const fn keyboard_interactivity(mut self, mode: KeyboardInteractivity) -> Self { + self.config.keyboard_interactivity = mode; + self + } + + #[must_use] + pub fn output_policy(mut self, policy: OutputPolicy) -> Self { + self.config.output_policy = policy; + self + } + + #[must_use] + pub fn window(self, component: impl Into) -> WindowConfigBuilder { + let shell_builder = self.complete(); + shell_builder.window(component) + } + + pub fn build(self) -> Result { + self.complete().build() + } + + pub fn run(self) -> Result<()> { + let mut runtime = self.build()?; + runtime.run() + } + + fn complete(mut self) -> ShellBuilder { + self.shell_builder.windows.push(WindowDefinition { + component: self.component, + config: self.config, + }); + self.shell_builder + } +} + +pub struct LayerShika; + +impl LayerShika { + pub fn from_file(path: impl AsRef) -> ShellBuilder { + ShellBuilder { + compilation: CompilationSource::File { + path: path.as_ref().to_path_buf(), + compiler: Compiler::default(), + }, + windows: Vec::new(), + } + } + + pub fn from_file_with_compiler(path: impl AsRef, compiler: Compiler) -> ShellBuilder { + ShellBuilder { + compilation: CompilationSource::File { + path: path.as_ref().to_path_buf(), + compiler, + }, + windows: Vec::new(), + } + } + + pub fn from_source(code: impl Into) -> ShellBuilder { + ShellBuilder { + compilation: CompilationSource::Source { + code: code.into(), + compiler: Compiler::default(), + }, + windows: Vec::new(), + } + } + + pub fn from_source_with_compiler(code: impl Into, compiler: Compiler) -> ShellBuilder { + ShellBuilder { + compilation: CompilationSource::Source { + code: code.into(), + compiler, + }, + windows: Vec::new(), + } + } + + pub fn from_compilation(result: Rc) -> ShellBuilder { + ShellBuilder { + compilation: CompilationSource::Compiled(result), + windows: Vec::new(), + } + } + + pub fn compile_file(path: impl AsRef) -> Result> { + let compiler = Compiler::default(); + let result = spin_on(compiler.build_from_path(path.as_ref())); + let diagnostics: Vec<_> = result.diagnostics().collect(); + if !diagnostics.is_empty() { + let messages: Vec = diagnostics.iter().map(ToString::to_string).collect(); + return Err(DomainError::Configuration { + message: format!( + "Failed to compile Slint file '{}':\n{}", + path.as_ref().display(), + messages.join("\n") + ), + } + .into()); + } + Ok(Rc::new(result)) + } + + pub fn compile_source(code: impl Into) -> Result> { + let compiler = Compiler::default(); + let result = spin_on(compiler.build_from_source(code.into(), PathBuf::default())); + let diagnostics: Vec<_> = result.diagnostics().collect(); + if !diagnostics.is_empty() { + let messages: Vec = diagnostics.iter().map(ToString::to_string).collect(); + return Err(DomainError::Configuration { + message: format!("Failed to compile Slint source:\n{}", messages.join("\n")), + } + .into()); + } + Ok(Rc::new(result)) + } +} + +pub struct Runtime { + inner: Rc>, + windows: HashMap, + compilation_result: Rc, + popup_command_sender: channel::Sender, +} + +impl Runtime { + pub(crate) fn new( + compilation_result: Rc, + definitions: Vec, + ) -> Result { + log::info!( + "Creating LayerShika runtime with {} windows", + definitions.len() + ); + + if definitions.is_empty() { + return Err(Error::Domain(DomainError::Configuration { + message: "At least one window definition is required".to_string(), + })); + } + + let is_single_window = definitions.len() == 1; + + if is_single_window { + let definition = definitions.into_iter().next().ok_or_else(|| { + Error::Domain(DomainError::Configuration { + message: "Expected at least one window definition".to_string(), + }) + })?; + Self::new_single_window(compilation_result, definition) + } else { + Self::new_multi_window(compilation_result, definitions) + } + } + + fn new_single_window( + compilation_result: Rc, + definition: WindowDefinition, + ) -> Result { + let component_definition = compilation_result + .component(&definition.component) + .ok_or_else(|| { + Error::Domain(DomainError::Configuration { + message: format!( + "Component '{}' not found in compilation result", + definition.component + ), + }) + })?; + + let wayland_config = WaylandWindowConfig::from_domain_config( + component_definition, + Some(Rc::clone(&compilation_result)), + definition.config.clone(), + ); + + let inner = layer_shika_adapters::WaylandWindowingSystem::new(&wayland_config)?; + let facade = WindowingSystemFacade::new(inner); + let inner_rc = Rc::new(RefCell::new(facade)); + + let (sender, receiver) = channel::channel(); + + let mut windows = HashMap::new(); + windows.insert(definition.component.clone(), definition); + + let shell = Self { + inner: Rc::clone(&inner_rc), + windows, + compilation_result, + popup_command_sender: sender, + }; + + shell.setup_popup_command_handler(receiver)?; + + log::info!("LayerShika runtime created (single-window mode)"); + + Ok(shell) + } + + fn new_multi_window( + compilation_result: Rc, + definitions: Vec, + ) -> Result { + let shell_configs: Vec = definitions + .iter() + .map(|def| { + let component_definition = compilation_result + .component(&def.component) + .ok_or_else(|| { + Error::Domain(DomainError::Configuration { + message: format!( + "Component '{}' not found in compilation result", + def.component + ), + }) + })?; + + let wayland_config = WaylandWindowConfig::from_domain_config( + component_definition, + Some(Rc::clone(&compilation_result)), + def.config.clone(), + ); + + Ok(ShellWindowConfig { + name: def.component.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 definition in definitions { + windows.insert(definition.component.clone(), definition); + } + + let shell = Self { + inner: Rc::clone(&inner_rc), + windows, + compilation_result, + popup_command_sender: sender, + }; + + shell.setup_popup_command_handler(receiver)?; + + log::info!( + "LayerShika runtime created (multi-window mode) with windows: {:?}", + shell.window_names() + ); + + Ok(shell) + } + + 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 = crate::system::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 window_names(&self) -> Vec<&str> { + self.windows.keys().map(String::as_str).collect() + } + + pub fn has_window(&self, name: &str) -> bool { + self.windows.contains_key(name) + } + + pub fn event_loop_handle(&self) -> EventLoopHandle { + EventLoopHandle::new(Rc::downgrade(&self.inner)) + } + + pub fn run(&mut self) -> Result<()> { + log::info!( + "Starting LayerShika event loop with {} windows", + self.windows.len() + ); + self.inner.borrow_mut().run()?; + Ok(()) + } + + pub fn with_window(&self, name: &str, f: F) -> Result + where + F: FnOnce(&ComponentInstance) -> R, + { + if !self.windows.contains_key(name) { + return Err(Error::Domain(DomainError::Configuration { + message: format!("Window '{}' not found", name), + })); + } + + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + + system + .app_state() + .windows_by_shell_name(name) + .next() + .map(|window| f(window.component_instance())) + .ok_or_else(|| { + Error::Domain(DomainError::Configuration { + message: format!("No instance found for window '{}'", name), + }) + }) + } + + pub fn with_all_windows(&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() { + for window in system.app_state().windows_by_shell_name(name) { + f(name, window.component_instance()); + } + } + } + + pub fn with_output(&self, handle: OutputHandle, f: F) -> Result + where + F: FnOnce(&ComponentInstance) -> R, + { + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + let window = system + .app_state() + .get_output_by_handle(handle) + .ok_or_else(|| { + Error::Domain(DomainError::Configuration { + message: format!("Output with handle {:?} not found", handle), + }) + })?; + Ok(f(window.component_instance())) + } + + pub fn with_all_outputs(&self, mut f: F) + where + F: FnMut(OutputHandle, &ComponentInstance), + { + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + for (handle, window) in system.app_state().outputs_with_handles() { + f(handle, window.component_instance()); + } + } + + #[must_use] + pub fn compilation_result(&self) -> &Rc { + &self.compilation_result + } + + #[must_use] + pub fn popup(&self, component_name: impl Into) -> PopupBuilder<'_> { + PopupBuilder::new(self, component_name.into()) + } + + pub fn on(&self, window_name: &str, callback_name: &str, handler: F) -> Result<()> + where + F: Fn(ShellControl) -> R + 'static, + R: IntoValue, + { + if !self.windows.contains_key(window_name) { + return Err(Error::Domain(DomainError::Configuration { + message: format!("Window '{}' not found", window_name), + })); + } + + let control = self.control(); + let handler = Rc::new(handler); + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + + for window in system.app_state().windows_by_shell_name(window_name) { + let handler_rc = Rc::clone(&handler); + let control_clone = control.clone(); + if let Err(e) = window + .component_instance() + .set_callback(callback_name, move |_args| { + handler_rc(control_clone.clone()).into_value() + }) + { + log::error!( + "Failed to register callback '{}' on window '{}': {}", + callback_name, + window_name, + e + ); + } + } + + Ok(()) + } + + pub fn on_with_args( + &self, + window_name: &str, + callback_name: &str, + handler: F, + ) -> Result<()> + where + F: Fn(&[Value], ShellControl) -> R + 'static, + R: IntoValue, + { + if !self.windows.contains_key(window_name) { + return Err(Error::Domain(DomainError::Configuration { + message: format!("Window '{}' not found", window_name), + })); + } + + let control = self.control(); + let handler = Rc::new(handler); + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + + for window in system.app_state().windows_by_shell_name(window_name) { + let handler_rc = Rc::clone(&handler); + let control_clone = control.clone(); + if let Err(e) = window + .component_instance() + .set_callback(callback_name, move |args| { + handler_rc(args, control_clone.clone()).into_value() + }) + { + log::error!( + "Failed to register callback '{}' on window '{}': {}", + callback_name, + window_name, + e + ); + } + } + + Ok(()) + } + + pub fn on_global(&self, callback_name: &str, handler: F) -> Result<()> + where + F: Fn(ShellControl) -> R + 'static, + R: IntoValue, + { + let control = self.control(); + let handler = Rc::new(handler); + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + + for window in system.app_state().all_outputs() { + let handler_rc = Rc::clone(&handler); + let control_clone = control.clone(); + if let Err(e) = window + .component_instance() + .set_callback(callback_name, move |_args| { + handler_rc(control_clone.clone()).into_value() + }) + { + log::error!( + "Failed to register global callback '{}': {}", + callback_name, + e + ); + } + } + + Ok(()) + } + + pub fn on_global_with_args(&self, callback_name: &str, handler: F) -> Result<()> + where + F: Fn(&[Value], ShellControl) -> R + 'static, + R: IntoValue, + { + let control = self.control(); + let handler = Rc::new(handler); + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + + for window in system.app_state().all_outputs() { + let handler_rc = Rc::clone(&handler); + let control_clone = control.clone(); + if let Err(e) = window + .component_instance() + .set_callback(callback_name, move |args| { + handler_rc(args, control_clone.clone()).into_value() + }) + { + log::error!( + "Failed to register global callback '{}': {}", + callback_name, + e + ); + } + } + + Ok(()) + } + + pub fn apply_window_config(&self, window_name: &str, f: F) + where + F: Fn(&ComponentInstance, LayerSurfaceHandle<'_>), + { + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + + if self.windows.contains_key(window_name) { + for window in system.app_state().windows_by_shell_name(window_name) { + let surface_handle = LayerSurfaceHandle::from_window_state(window); + f(window.component_instance(), surface_handle); + } + } + } + + pub fn apply_global_config(&self, f: F) + where + F: Fn(&ComponentInstance, LayerSurfaceHandle<'_>), + { + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + + for window in system.app_state().all_outputs() { + let surface_handle = LayerSurfaceHandle::from_window_state(window); + f(window.component_instance(), surface_handle); + } + } + + pub fn output_registry(&self) -> OutputRegistry { + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + system.app_state().output_registry().clone() + } + + pub fn get_output_info(&self, handle: OutputHandle) -> Option { + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + system.app_state().get_output_info(handle).cloned() + } + + pub fn all_output_info(&self) -> Vec { + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + system.app_state().all_output_info().cloned().collect() + } +} + +impl ShellRuntime for Runtime { + type LoopHandle = EventLoopHandle; + type Context<'a> = EventContext<'a>; + + fn event_loop_handle(&self) -> Self::LoopHandle { + EventLoopHandle::new(Rc::downgrade(&self.inner)) + } + + fn with_component(&self, name: &str, mut f: F) + where + F: FnMut(&ComponentInstance), + { + let facade = self.inner.borrow(); + let system = facade.inner_ref(); + + if self.windows.contains_key(name) { + for window in system.app_state().windows_by_shell_name(name) { + f(window.component_instance()); + } + } + } + + 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() { + for window in system.app_state().windows_by_shell_name(name) { + f(name, window.component_instance()); + } + } + } + + fn run(&mut self) -> Result<()> { + self.inner.borrow_mut().run()?; + Ok(()) + } +} + +pub type EventLoopHandle = EventLoopHandleBase>; + +pub struct EventContext<'a> { + app_state: &'a mut AppState, +} + +impl<'a> FromAppState<'a> for EventContext<'a> { + fn from_app_state(app_state: &'a mut AppState) -> Self { + Self { app_state } + } +} + +impl EventContext<'_> { + pub fn get_window_component(&self, name: &str) -> Option<&ComponentInstance> { + self.app_state + .windows_by_shell_name(name) + .next() + .map(WindowState::component_instance) + } + + pub fn all_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(()) + } + + #[must_use] + pub fn primary_output_handle(&self) -> Option { + self.app_state.primary_output_handle() + } + + #[must_use] + pub fn active_output_handle(&self) -> Option { + self.app_state.active_output_handle() + } + + pub fn output_registry(&self) -> &OutputRegistry { + self.app_state.output_registry() + } + + pub fn outputs(&self) -> impl Iterator { + self.app_state + .outputs_with_handles() + .map(|(handle, window)| (handle, window.component_instance())) + } + + pub fn get_output_component(&self, handle: OutputHandle) -> Option<&ComponentInstance> { + self.app_state + .get_output_by_handle(handle) + .map(WindowState::component_instance) + } + + pub fn get_output_info(&self, handle: OutputHandle) -> Option<&OutputInfo> { + self.app_state.get_output_info(handle) + } + + pub fn all_output_info(&self) -> impl Iterator { + self.app_state.all_output_info() + } + + pub fn outputs_with_info(&self) -> impl Iterator { + self.app_state + .outputs_with_info() + .map(|(info, window)| (info, window.component_instance())) + } + + #[must_use] + pub fn compilation_result(&self) -> Option> { + self.app_state + .primary_output() + .and_then(WindowState::compilation_result) + } +} diff --git a/crates/composition/src/lib.rs b/crates/composition/src/lib.rs index c76d1fe..f359cda 100644 --- a/crates/composition/src/lib.rs +++ b/crates/composition/src/lib.rs @@ -1,10 +1,9 @@ #![allow(clippy::pub_use)] -mod builder; mod event_loop; +mod layer_shika; mod popup_builder; mod shell; -mod shell_composition; mod shell_runtime; mod system; pub mod value_conversion; @@ -13,7 +12,6 @@ use layer_shika_adapters::errors::LayerShikaError; use layer_shika_domain::errors::DomainError; use std::result::Result as StdResult; -pub use builder::LayerShika; pub use layer_shika_adapters::PopupWindow; pub use layer_shika_adapters::platform::{slint, slint_interpreter}; pub use layer_shika_domain::entities::output_registry::OutputRegistry; @@ -37,7 +35,12 @@ pub use shell::{ LayerSurfaceHandle, Shell, ShellEventContext, ShellEventLoopHandle, ShellWindowConfigHandler, ShellWindowHandle, }; -pub use shell_composition::{ShellComposition, ShellWindowDefinition}; + +pub use layer_shika::{ + DEFAULT_COMPONENT_NAME, EventContext as LayerShikaEventContext, + EventLoopHandle as LayerShikaEventLoopHandle, LayerShika, Runtime, ShellBuilder, + WindowConfigBuilder, WindowDefinition, +}; pub mod calloop { pub use layer_shika_adapters::platform::calloop::{ @@ -61,16 +64,17 @@ pub enum Error { pub mod prelude { pub use crate::{ - AnchorEdges, AnchorStrategy, DEFAULT_WINDOW_NAME, EventContext, EventLoopHandle, IntoValue, - KeyboardInteractivity, Layer, LayerShika, OutputGeometry, OutputHandle, OutputInfo, - OutputPolicy, OutputRegistry, PopupBuilder, PopupHandle, PopupPlacement, - PopupPositioningMode, PopupRequest, PopupSize, PopupWindow, Result, ShellControl, - ShellRuntime, SingleWindowShell, + AnchorEdges, AnchorStrategy, DEFAULT_COMPONENT_NAME, DEFAULT_WINDOW_NAME, EventContext, + EventLoopHandle, IntoValue, KeyboardInteractivity, Layer, LayerShika, + LayerShikaEventContext, LayerShikaEventLoopHandle, OutputGeometry, OutputHandle, + OutputInfo, OutputPolicy, OutputRegistry, PopupBuilder, PopupHandle, PopupPlacement, + PopupPositioningMode, PopupRequest, PopupSize, PopupWindow, Result, Runtime, ShellBuilder, + ShellControl, ShellRuntime, SingleWindowShell, WindowConfigBuilder, WindowDefinition, }; pub use crate::{ - LayerSurfaceHandle, Shell, ShellComposition, ShellEventContext, ShellEventLoopHandle, - ShellWindowConfigHandler, ShellWindowDefinition, ShellWindowHandle, + LayerSurfaceHandle, Shell, ShellEventContext, ShellEventLoopHandle, + ShellWindowConfigHandler, ShellWindowHandle, }; pub use crate::calloop::{Generic, Interest, Mode, PostAction, RegistrationToken, Timer}; diff --git a/crates/composition/src/popup_builder.rs b/crates/composition/src/popup_builder.rs index 97babe2..efcaf16 100644 --- a/crates/composition/src/popup_builder.rs +++ b/crates/composition/src/popup_builder.rs @@ -1,12 +1,12 @@ use crate::Result; -use crate::system::SingleWindowShell; +use crate::layer_shika::Runtime; use layer_shika_adapters::platform::slint_interpreter::Value; use layer_shika_domain::prelude::AnchorStrategy; use layer_shika_domain::value_objects::popup_positioning_mode::PopupPositioningMode; use layer_shika_domain::value_objects::popup_request::{PopupPlacement, PopupRequest, PopupSize}; pub struct PopupBuilder<'a> { - shell: &'a SingleWindowShell, + shell: &'a Runtime, component: String, reference: PopupPlacement, anchor: PopupPositioningMode, @@ -17,7 +17,7 @@ pub struct PopupBuilder<'a> { } impl<'a> PopupBuilder<'a> { - pub(crate) fn new(shell: &'a SingleWindowShell, component: String) -> Self { + pub(crate) fn new(shell: &'a Runtime, component: String) -> Self { Self { shell, component, @@ -148,7 +148,7 @@ impl<'a> PopupBuilder<'a> { let request = self.build_request(); let control = self.shell.control(); - self.shell.with_all_component_instances(|instance| { + self.shell.with_all_windows(|_name, instance| { let request_clone = request.clone(); let control_clone = control.clone(); @@ -174,7 +174,7 @@ impl<'a> PopupBuilder<'a> { let control = self.shell.control(); let component_name = request.component.clone(); - self.shell.with_all_component_instances(|instance| { + self.shell.with_all_windows(|_name, instance| { let request_clone = request.clone(); let control_clone = control.clone(); let component_clone = component_name.clone(); @@ -205,7 +205,7 @@ impl<'a> PopupBuilder<'a> { let resize_callback = self.resize_callback.clone(); let control = self.shell.control(); - self.shell.with_all_component_instances(|instance| { + self.shell.with_all_windows(|_name, instance| { let component_clone = component_name.clone(); let control_clone = control.clone(); let close_cb = close_callback.clone(); diff --git a/crates/composition/src/shell.rs b/crates/composition/src/shell.rs index 7d33b46..db4f3de 100644 --- a/crates/composition/src/shell.rs +++ b/crates/composition/src/shell.rs @@ -1,5 +1,5 @@ use crate::event_loop::{EventLoopHandleBase, FromAppState}; -use crate::shell_composition::ShellWindowDefinition; +use crate::layer_shika::WindowDefinition; use crate::shell_runtime::ShellRuntime; use crate::system::{EventContext, PopupCommand, ShellControl}; use crate::value_conversion::IntoValue; @@ -29,7 +29,11 @@ pub struct LayerSurfaceHandle<'a> { window_state: &'a WindowState, } -impl LayerSurfaceHandle<'_> { +impl<'a> LayerSurfaceHandle<'a> { + pub(crate) fn from_window_state(window_state: &'a WindowState) -> Self { + Self { window_state } + } + pub fn set_anchor(&self, anchor: Anchor) { self.window_state.layer_surface().set_anchor(anchor); } @@ -102,10 +106,11 @@ pub struct Shell { popup_command_sender: channel::Sender, } +#[allow(dead_code)] impl Shell { pub(crate) fn new( compilation_result: Rc, - definitions: &[ShellWindowDefinition], + definitions: &[WindowDefinition], ) -> Result { log::info!("Creating shell with {} windows", definitions.len()); @@ -119,12 +124,12 @@ impl Shell { .iter() .map(|def| { let component_definition = compilation_result - .component(&def.component_name) + .component(&def.component) .ok_or_else(|| { Error::Domain(DomainError::Configuration { message: format!( "Component '{}' not found in compilation result", - def.component_name + def.component ), }) })?; @@ -136,7 +141,7 @@ impl Shell { ); Ok(ShellWindowConfig { - name: def.component_name.clone(), + name: def.component.clone(), config: wayland_config, }) }) @@ -151,9 +156,9 @@ impl Shell { let mut windows = HashMap::new(); for def in definitions { windows.insert( - def.component_name.clone(), + def.component.clone(), ShellWindowHandle { - name: def.component_name.clone(), + name: def.component.clone(), }, ); } diff --git a/crates/composition/src/shell_composition.rs b/crates/composition/src/shell_composition.rs deleted file mode 100644 index 7e6fa84..0000000 --- a/crates/composition/src/shell_composition.rs +++ /dev/null @@ -1,79 +0,0 @@ -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 with_window(mut self, component_name: impl Into, config: WindowConfig) -> Self { - self.shell_windows.push(ShellWindowDefinition { - component_name: component_name.into(), - config, - }); - self - } - - pub fn with_windows(mut self, definitions: Vec) -> Self { - self.shell_windows.extend(definitions); - self - } - - pub fn with_default_config_for(mut self, components: Vec>) -> Self { - self.auto_discover_components = components.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 add_window(), add_windows(), or with_default_config_for()".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 442bbd5..2c2d5cb 100644 --- a/crates/composition/src/system.rs +++ b/crates/composition/src/system.rs @@ -1,5 +1,4 @@ use crate::event_loop::{EventLoopHandleBase, FromAppState}; -use crate::popup_builder::PopupBuilder; use crate::shell_runtime::{DEFAULT_WINDOW_NAME, ShellRuntime}; use crate::value_conversion::IntoValue; use crate::{Error, Result}; @@ -559,6 +558,7 @@ pub struct SingleWindowShell { window_name: String, } +#[allow(dead_code)] impl SingleWindowShell { pub(crate) fn new( component_definition: ComponentDefinition, @@ -756,11 +756,6 @@ impl SingleWindowShell { Ok(()) } - #[must_use] - pub fn popup(&self, component_name: impl Into) -> PopupBuilder<'_> { - PopupBuilder::new(self, component_name.into()) - } - pub fn run(&mut self) -> Result<()> { self.inner.borrow_mut().run()?; Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 581e8d4..32d23a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,35 +30,58 @@ //! //! # Quick Start //! +//! Single-window use case: +//! //! ```rust,no_run //! use layer_shika::prelude::*; //! -//! LayerShika::from_file("ui/main.slint")? -//! .height(42) -//! .anchor(AnchorEdges::top_bar()) -//! .exclusive_zone(42) +//! LayerShika::from_file("ui/bar.slint") +//! .window("Main") +//! .height(42) +//! .anchor(AnchorEdges::top_bar()) +//! .exclusive_zone(42) +//! .build()? //! .run()?; //! # Ok::<(), layer_shika::Error>(()) //! ``` //! //! # Multi-Window Shell //! -//! For multi-window shell applications: +//! Same API naturally extends to multiple windows: //! //! ```rust,no_run //! use layer_shika::prelude::*; -//! use std::rc::Rc; //! -//! // Load Slint file with multiple shell window components -//! let compilation_result = Rc::new(/* ... */); +//! LayerShika::from_file("ui/shell.slint") +//! .window("TopBar") +//! .height(42) +//! .anchor(AnchorEdges::top_bar()) +//! .window("Dock") +//! .height(64) +//! .anchor(AnchorEdges::bottom_bar()) +//! .build()? +//! .run()?; +//! # Ok::<(), layer_shika::Error>(()) +//! ``` //! -//! // Create shell with typed WindowConfig -//! let shell = ShellComposition::new() -//! .with_compilation_result(compilation_result) -//! .with_window("TopBar", WindowConfig::default()) -//! .build()?; +//! # Pre-compiled Slint //! -//! shell.run()?; +//! For explicit compilation control: +//! +//! ```rust,no_run +//! use layer_shika::prelude::*; +//! +//! let compilation = LayerShika::compile_file("ui/shell.slint")?; +//! +//! LayerShika::from_compilation(compilation) +//! .window("TopBar") +//! .output_policy(OutputPolicy::AllOutputs) +//! .height(42) +//! .window("Dock") +//! .output_policy(OutputPolicy::PrimaryOnly) +//! .height(64) +//! .build()? +//! .run()?; //! # Ok::<(), layer_shika::Error>(()) //! ``` @@ -75,9 +98,10 @@ pub mod window; pub use layer_shika_composition::{Error, Result}; pub use shell::{ - DEFAULT_WINDOW_NAME, LayerShika, LayerSurfaceHandle, Shell, ShellComposition, ShellControl, - ShellEventContext, ShellRuntime, ShellWindowConfigHandler, ShellWindowDefinition, - ShellWindowHandle, SingleWindowShell, + DEFAULT_COMPONENT_NAME, DEFAULT_WINDOW_NAME, LayerShika, LayerShikaEventContext, + LayerShikaEventLoopHandle, LayerSurfaceHandle, Runtime, Shell, ShellBuilder, ShellControl, + ShellEventContext, ShellRuntime, ShellWindowConfigHandler, ShellWindowHandle, + SingleWindowShell, WindowConfigBuilder, WindowDefinition, }; pub use window::{ diff --git a/src/prelude.rs b/src/prelude.rs index d8372f4..8e62848 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -9,9 +9,10 @@ #![allow(clippy::pub_use)] pub use crate::shell::{ - DEFAULT_WINDOW_NAME, LayerShika, LayerSurfaceHandle, Shell, ShellComposition, ShellControl, + DEFAULT_COMPONENT_NAME, DEFAULT_WINDOW_NAME, LayerShika, LayerShikaEventContext, + LayerShikaEventLoopHandle, LayerSurfaceHandle, Runtime, Shell, ShellBuilder, ShellControl, ShellEventContext, ShellEventLoopHandle, ShellRuntime, ShellWindowConfigHandler, - ShellWindowDefinition, ShellWindowHandle, SingleWindowShell, + ShellWindowHandle, SingleWindowShell, WindowConfigBuilder, WindowDefinition, }; pub use crate::window::{ diff --git a/src/shell.rs b/src/shell.rs index 3797b36..84a1484 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -1,5 +1,6 @@ pub use layer_shika_composition::{ - DEFAULT_WINDOW_NAME, LayerShika, LayerSurfaceHandle, Shell, ShellComposition, ShellControl, + DEFAULT_COMPONENT_NAME, DEFAULT_WINDOW_NAME, LayerShika, LayerShikaEventContext, + LayerShikaEventLoopHandle, LayerSurfaceHandle, Runtime, Shell, ShellBuilder, ShellControl, ShellEventContext, ShellEventLoopHandle, ShellRuntime, ShellWindowConfigHandler, - ShellWindowDefinition, ShellWindowHandle, SingleWindowShell, + ShellWindowHandle, SingleWindowShell, WindowConfigBuilder, WindowDefinition, };