feat: introducing base multi-window support

This commit is contained in:
drendog 2025-12-03 03:18:15 +01:00
parent a2c376e8fc
commit 736d4d0905
Signed by: dwenya
GPG key ID: 8DD77074645332D0
22 changed files with 1154 additions and 121 deletions

View file

@ -6,7 +6,7 @@ pub(crate) mod wayland;
pub use rendering::femtovg::popup_window::PopupWindow; 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::facade::WindowingSystemFacade;
pub use wayland::shell_adapter::WaylandWindowingSystem; pub use wayland::shell_adapter::WaylandWindowingSystem;
pub use wayland::surfaces::app_state::AppState; pub use wayland::surfaces::app_state::AppState;
@ -25,4 +25,8 @@ pub mod platform {
EventSource, InsertError, Interest, Mode, PostAction, RegistrationToken, 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;
}
} }

View file

@ -18,11 +18,13 @@ pub(crate) struct LayerSurfaceConfig {
pub exclusive_zone: i32, pub exclusive_zone: i32,
pub keyboard_interactivity: WaylandKeyboardInteractivity, pub keyboard_interactivity: WaylandKeyboardInteractivity,
pub height: u32, pub height: u32,
pub width: u32,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct WaylandWindowConfig { pub struct WaylandWindowConfig {
pub height: u32, pub height: u32,
pub width: u32,
pub layer: zwlr_layer_shell_v1::Layer, pub layer: zwlr_layer_shell_v1::Layer,
pub margin: Margins, pub margin: Margins,
pub anchor: Anchor, pub anchor: Anchor,
@ -43,7 +45,8 @@ impl WaylandWindowConfig {
domain_config: DomainWindowConfig, domain_config: DomainWindowConfig,
) -> Self { ) -> Self {
Self { Self {
height: domain_config.height.value(), height: domain_config.dimensions.height(),
width: domain_config.dimensions.width(),
layer: convert_layer(domain_config.layer), layer: convert_layer(domain_config.layer),
margin: domain_config.margin, margin: domain_config.margin,
anchor: convert_anchor(domain_config.anchor), anchor: convert_anchor(domain_config.anchor),
@ -97,3 +100,37 @@ const fn convert_keyboard_interactivity(
DomainKeyboardInteractivity::OnDemand => WaylandKeyboardInteractivity::OnDemand, DomainKeyboardInteractivity::OnDemand => WaylandKeyboardInteractivity::OnDemand,
} }
} }
#[derive(Clone)]
pub struct ShellWindowConfig {
pub name: String,
pub config: WaylandWindowConfig,
}
#[derive(Clone)]
pub struct MultiWindowConfig {
pub windows: Vec<ShellWindowConfig>,
pub compilation_result: Rc<CompilationResult>,
}
impl MultiWindowConfig {
pub fn new(compilation_result: Rc<CompilationResult>) -> Self {
Self {
windows: Vec::new(),
compilation_result,
}
}
#[must_use]
pub fn add_window(mut self, name: impl Into<String>, 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)
}
}

View file

@ -98,7 +98,7 @@ impl Dispatch<WlOutput, ()> for AppState {
width, height, is_current, is_preferred width, height, is_current, is_preferred
); );
if is_current { 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); window.handle_output_mode(width, height);
} }
} }
@ -202,21 +202,16 @@ impl Dispatch<WlPointer, ()> for AppState {
let surface_id = surface.id(); let surface_id = surface.id();
if let Some(window) = state.get_output_by_surface_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); 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 { state.set_active_window_key(Some(key));
let handle = state.get_handle_by_popup(&surface_id); } else if let Some(key) = state.get_key_by_popup(&surface_id).cloned() {
if let Some(window) = state.find_output_by_popup_mut(&surface_id) { if let Some(window) = state.get_window_by_key_mut(&key) {
window.handle_pointer_enter(serial, &surface, surface_x, surface_y); 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));
} }
} }
@ -225,20 +220,16 @@ impl Dispatch<WlPointer, ()> for AppState {
surface_y, surface_y,
.. ..
} => { } => {
if let Some(output_handle) = state.active_output_handle() { if let Some(window) = state.active_window_mut() {
if let Some(window) = state.get_output_by_handle_mut(output_handle) {
window.handle_pointer_motion(surface_x, surface_y); window.handle_pointer_motion(surface_x, surface_y);
} }
} }
}
wl_pointer::Event::Leave { .. } => { wl_pointer::Event::Leave { .. } => {
if let Some(output_handle) = state.active_output_handle() { if let Some(window) = state.active_window_mut() {
if let Some(window) = state.get_output_by_handle_mut(output_handle) {
window.handle_pointer_leave(); window.handle_pointer_leave();
} }
} state.set_active_window_key(None);
state.set_active_output_handle(None);
} }
wl_pointer::Event::Button { wl_pointer::Event::Button {
@ -246,12 +237,10 @@ impl Dispatch<WlPointer, ()> for AppState {
state: button_state, state: button_state,
.. ..
} => { } => {
if let Some(output_handle) = state.active_output_handle() { if let Some(window) = state.active_window_mut() {
if let Some(window) = state.get_output_by_handle_mut(output_handle) {
window.handle_pointer_button(serial, button_state); window.handle_pointer_button(serial, button_state);
} }
} }
}
_ => {} _ => {}
} }
} }

View file

@ -144,7 +144,7 @@ impl OutputManager {
let (window, main_surface_id) = let (window, main_surface_id) =
self.create_window_for_output(&pending_output.proxy, output_id, queue_handle)?; 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(()) Ok(())
} }
@ -182,6 +182,7 @@ impl OutputManager {
.with_layer_surface(Rc::clone(&surface_ctx.layer_surface)) .with_layer_surface(Rc::clone(&surface_ctx.layer_surface))
.with_scale_factor(self.config.scale_factor) .with_scale_factor(self.config.scale_factor)
.with_height(self.config.height) .with_height(self.config.height)
.with_width(self.config.width)
.with_exclusive_zone(self.config.exclusive_zone) .with_exclusive_zone(self.config.exclusive_zone)
.with_connection(Rc::clone(self.context.connection())) .with_connection(Rc::clone(self.context.connection()))
.with_pointer(Rc::clone(&self.context.pointer)) .with_pointer(Rc::clone(&self.context.pointer))
@ -215,10 +216,14 @@ impl OutputManager {
if let Some(handle) = self.output_mapping.remove(output_id) { if let Some(handle) = self.output_mapping.remove(output_id) {
info!("Removing output {handle:?} (id: {output_id:?})"); info!("Removing output {handle:?} (id: {output_id:?})");
if let Some(_window) = app_state.remove_output(handle) { let removed_windows = app_state.remove_output(handle);
info!("Cleaned up window for output {handle:?}"); if removed_windows.is_empty() {
} else {
warn!("No window found for output handle {handle:?}"); warn!("No window found for output handle {handle:?}");
} else {
info!(
"Cleaned up {} window(s) for output {handle:?}",
removed_windows.len()
);
} }
} else { } else {
self.pending_outputs.borrow_mut().remove(output_id); self.pending_outputs.borrow_mut().remove(output_id);

View file

@ -1,5 +1,5 @@
use crate::wayland::{ use crate::wayland::{
config::{LayerSurfaceConfig, WaylandWindowConfig}, config::{LayerSurfaceConfig, ShellWindowConfig, WaylandWindowConfig},
globals::context::GlobalContext, globals::context::GlobalContext,
managed_proxies::ManagedWlPointer, managed_proxies::ManagedWlPointer,
outputs::{OutputManager, OutputManagerContext}, outputs::{OutputManager, OutputManagerContext},
@ -50,6 +50,7 @@ struct OutputSetup {
main_surface_id: ObjectId, main_surface_id: ObjectId,
window: Rc<FemtoVGWindow>, window: Rc<FemtoVGWindow>,
builder: WindowStateBuilder, builder: WindowStateBuilder,
shell_window_name: String,
} }
struct OutputManagerParams<'a> { struct OutputManagerParams<'a> {
@ -87,6 +88,31 @@ impl WaylandWindowingSystem {
}) })
} }
pub fn new_multi(configs: &[ShellWindowConfig]) -> Result<Self> {
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<Connection>, EventQueue<AppState>)> { fn init_wayland_connection() -> Result<(Rc<Connection>, EventQueue<AppState>)> {
let connection = Rc::new(Connection::connect_to_env()?); let connection = Rc::new(Connection::connect_to_env()?);
let event_queue = connection.new_event_queue(); let event_queue = connection.new_event_queue();
@ -100,6 +126,7 @@ impl WaylandWindowingSystem {
exclusive_zone: config.exclusive_zone, exclusive_zone: config.exclusive_zone,
keyboard_interactivity: config.keyboard_interactivity, keyboard_interactivity: config.keyboard_interactivity,
height: config.height, height: config.height,
width: config.width,
} }
} }
@ -152,6 +179,7 @@ impl WaylandWindowingSystem {
.with_layer_surface(Rc::clone(&surface_ctx.layer_surface)) .with_layer_surface(Rc::clone(&surface_ctx.layer_surface))
.with_scale_factor(config.scale_factor) .with_scale_factor(config.scale_factor)
.with_height(config.height) .with_height(config.height)
.with_width(config.width)
.with_exclusive_zone(config.exclusive_zone) .with_exclusive_zone(config.exclusive_zone)
.with_connection(Rc::new(connection.clone())) .with_connection(Rc::new(connection.clone()))
.with_pointer(Rc::clone(pointer)) .with_pointer(Rc::clone(pointer))
@ -170,6 +198,7 @@ impl WaylandWindowingSystem {
main_surface_id, main_surface_id,
window, window,
builder, builder,
shell_window_name: "default".to_string(),
}); });
} }
@ -222,7 +251,12 @@ impl WaylandWindowingSystem {
popup_managers.push(Rc::clone(&popup_manager)); popup_managers.push(Rc::clone(&popup_manager));
layer_surfaces.push(per_output_window.layer_surface()); 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)) Ok((popup_managers, layer_surfaces))
@ -295,6 +329,163 @@ impl WaylandWindowingSystem {
Ok(app_state) Ok(app_state)
} }
fn init_state_multi(
configs: &[ShellWindowConfig],
connection: &Connection,
event_queue: &mut EventQueue<AppState>,
) -> Result<AppState> {
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<AppState>,
pointer: &Rc<WlPointer>,
) -> Result<Vec<OutputSetup>> {
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 { fn create_output_manager(params: &OutputManagerParams<'_>) -> OutputManager {
let manager_context = OutputManagerContext { let manager_context = OutputManagerContext {
compositor: params.global_ctx.compositor.clone(), compositor: params.global_ctx.compositor.clone(),

View file

@ -13,15 +13,31 @@ use wayland_client::backend::ObjectId;
pub type PerOutputWindow = WindowState; 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<String>) -> Self {
Self {
output_handle,
shell_window_name: shell_window_name.into(),
}
}
}
pub struct AppState { pub struct AppState {
output_registry: OutputRegistry, output_registry: OutputRegistry,
output_mapping: OutputMapping, output_mapping: OutputMapping,
windows: HashMap<OutputHandle, PerOutputWindow>, windows: HashMap<ShellWindowKey, PerOutputWindow>,
surface_to_output: HashMap<ObjectId, OutputHandle>, surface_to_key: HashMap<ObjectId, ShellWindowKey>,
_pointer: ManagedWlPointer, _pointer: ManagedWlPointer,
shared_pointer_serial: Rc<SharedPointerSerial>, shared_pointer_serial: Rc<SharedPointerSerial>,
output_manager: Option<Rc<RefCell<OutputManager>>>, output_manager: Option<Rc<RefCell<OutputManager>>>,
registry_name_to_output_id: HashMap<u32, ObjectId>, registry_name_to_output_id: HashMap<u32, ObjectId>,
active_window_key: Option<ShellWindowKey>,
} }
impl AppState { impl AppState {
@ -30,11 +46,12 @@ impl AppState {
output_registry: OutputRegistry::new(), output_registry: OutputRegistry::new(),
output_mapping: OutputMapping::new(), output_mapping: OutputMapping::new(),
windows: HashMap::new(), windows: HashMap::new(),
surface_to_output: HashMap::new(), surface_to_key: HashMap::new(),
_pointer: pointer, _pointer: pointer,
shared_pointer_serial: shared_serial, shared_pointer_serial: shared_serial,
output_manager: None, output_manager: None,
registry_name_to_output_id: HashMap::new(), 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) self.registry_name_to_output_id.remove(&name)
} }
pub fn add_output( pub fn add_shell_window(
&mut self, &mut self,
output_id: ObjectId, output_id: &ObjectId,
shell_window_name: &str,
main_surface_id: ObjectId, main_surface_id: ObjectId,
window: PerOutputWindow, window: PerOutputWindow,
) { ) {
let handle = self.output_mapping.insert(output_id); let handle = self.output_mapping.get(output_id).unwrap_or_else(|| {
self.surface_to_output.insert(main_surface_id, handle); let h = self.output_mapping.insert(output_id.clone());
let is_primary = self.output_registry.is_empty(); let is_primary = self.output_registry.is_empty();
let mut info = OutputInfo::new(h);
let mut info = OutputInfo::new(handle);
info.set_primary(is_primary); info.set_primary(is_primary);
self.output_registry.add(info); self.output_registry.add(info);
self.windows.insert(handle, window); h
});
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<PerOutputWindow> { 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<PerOutputWindow> {
self.output_registry.remove(handle); 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 {
window if let Some(window) = self.windows.remove(&key) {
removed.push(window);
}
} }
pub fn get_output_by_handle(&self, handle: OutputHandle) -> Option<&PerOutputWindow> { self.surface_to_key.retain(|_, k| k.output_handle != handle);
self.windows.get(&handle)
removed
} }
pub fn get_output_by_handle_mut( pub fn get_window_by_key(&self, key: &ShellWindowKey) -> Option<&PerOutputWindow> {
self.windows.get(key)
}
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, &mut self,
handle: OutputHandle, output_handle: OutputHandle,
shell_window_name: &str,
) -> Option<&mut PerOutputWindow> { ) -> 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> { pub fn get_output_by_output_id(&self, output_id: &ObjectId) -> Option<&PerOutputWindow> {
self.output_mapping self.output_mapping
.get(output_id) .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( pub fn get_output_by_output_id_mut(
@ -109,22 +165,39 @@ impl AppState {
) -> Option<&mut PerOutputWindow> { ) -> Option<&mut PerOutputWindow> {
self.output_mapping self.output_mapping
.get(output_id) .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> { pub fn get_output_by_surface(&self, surface_id: &ObjectId) -> Option<&PerOutputWindow> {
self.surface_to_output self.surface_to_key
.get(surface_id) .get(surface_id)
.and_then(|handle| self.windows.get(handle)) .and_then(|key| self.windows.get(key))
} }
pub fn get_output_by_surface_mut( pub fn get_output_by_surface_mut(
&mut self, &mut self,
surface_id: &ObjectId, surface_id: &ObjectId,
) -> Option<&mut PerOutputWindow> { ) -> Option<&mut PerOutputWindow> {
self.surface_to_output self.surface_to_key
.get(surface_id) .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( 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) .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<OutputHandle> { pub fn get_handle_by_surface(&self, surface_id: &ObjectId) -> Option<OutputHandle> {
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<OutputHandle> { pub fn get_handle_by_output_id(&self, output_id: &ObjectId) -> Option<OutputHandle> {
@ -152,10 +231,28 @@ impl AppState {
self.output_registry.active_handle() self.output_registry.active_handle()
} }
pub fn set_active_window_key(&mut self, key: Option<ShellWindowKey>) {
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> { pub fn primary_output(&self) -> Option<&PerOutputWindow> {
self.output_registry self.output_registry
.primary_handle() .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<OutputHandle> { pub fn primary_output_handle(&self) -> Option<OutputHandle> {
@ -165,7 +262,7 @@ impl AppState {
pub fn active_output(&self) -> Option<&PerOutputWindow> { pub fn active_output(&self) -> Option<&PerOutputWindow> {
self.output_registry self.output_registry
.active_handle() .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<Item = &PerOutputWindow> { pub fn all_outputs(&self) -> impl Iterator<Item = &PerOutputWindow> {
@ -176,10 +273,18 @@ impl AppState {
self.windows.values_mut() self.windows.values_mut()
} }
pub fn outputs_with_handles(&self) -> impl Iterator<Item = (OutputHandle, &PerOutputWindow)> { pub fn windows_for_output(
&self,
handle: OutputHandle,
) -> impl Iterator<Item = (&str, &PerOutputWindow)> {
self.windows self.windows
.iter() .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<Item = (&ShellWindowKey, &PerOutputWindow)> {
self.windows.iter()
} }
pub const fn shared_pointer_serial(&self) -> &Rc<SharedPointerSerial> { pub const fn shared_pointer_serial(&self) -> &Rc<SharedPointerSerial> {
@ -209,16 +314,28 @@ impl AppState {
}) })
} }
pub fn get_handle_by_popup(&self, popup_surface_id: &ObjectId) -> Option<OutputHandle> { pub fn get_key_by_popup(&self, popup_surface_id: &ObjectId) -> Option<&ShellWindowKey> {
self.windows.iter().find_map(|(&handle, window)| { self.windows.iter().find_map(|(key, window)| {
window window
.popup_manager() .popup_manager()
.as_ref() .as_ref()
.and_then(|pm| pm.find_by_surface(popup_surface_id)) .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<OutputHandle> {
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> { pub fn get_output_info(&self, handle: OutputHandle) -> Option<&OutputInfo> {
self.output_registry.get(handle) self.output_registry.get(handle)
} }
@ -231,13 +348,61 @@ impl AppState {
self.output_registry.all_info() self.output_registry.all_info()
} }
pub fn outputs_with_info(&self) -> impl Iterator<Item = (&OutputInfo, &PerOutputWindow)> {
self.output_registry
.all()
.filter_map(|(handle, info)| self.windows.get(&handle).map(|window| (info, window)))
}
pub const fn output_registry(&self) -> &OutputRegistry { pub const fn output_registry(&self) -> &OutputRegistry {
&self.output_registry &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<Item = &PerOutputWindow> {
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<Item = (OutputHandle, &PerOutputWindow)> {
self.windows
.iter()
.map(|(key, window)| (key.output_handle, window))
}
pub fn outputs_with_info(&self) -> impl Iterator<Item = (&OutputInfo, &PerOutputWindow)> {
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()
}
} }

View file

@ -90,7 +90,8 @@ impl SurfaceCtx {
layer_surface.set_exclusive_zone(config.exclusive_zone); layer_surface.set_exclusive_zone(config.exclusive_zone);
layer_surface.set_keyboard_interactivity(config.keyboard_interactivity); 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(); surface.commit();
} }
} }

View file

@ -49,4 +49,8 @@ impl<W: RenderableWindow> RenderingState<W> {
pub fn fractional_scale(&self) -> Option<&ManagedWpFractionalScaleV1> { pub fn fractional_scale(&self) -> Option<&ManagedWpFractionalScaleV1> {
self.renderer.fractional_scale() self.renderer.fractional_scale()
} }
pub fn commit_surface(&self) {
self.renderer.commit_surface();
}
} }

View file

@ -37,6 +37,7 @@ pub struct WindowStateBuilder {
pub connection: Option<Rc<Connection>>, pub connection: Option<Rc<Connection>>,
pub scale_factor: f32, pub scale_factor: f32,
pub height: u32, pub height: u32,
pub width: u32,
pub exclusive_zone: i32, pub exclusive_zone: i32,
} }
@ -100,6 +101,12 @@ impl WindowStateBuilder {
self self
} }
#[must_use]
pub const fn with_width(mut self, width: u32) -> Self {
self.width = width;
self
}
#[must_use] #[must_use]
pub fn with_component_definition(mut self, component_definition: ComponentDefinition) -> Self { pub fn with_component_definition(mut self, component_definition: ComponentDefinition) -> Self {
self.component_definition = Some(component_definition); self.component_definition = Some(component_definition);
@ -163,6 +170,7 @@ impl Default for WindowStateBuilder {
connection: None, connection: None,
scale_factor: 1.0, scale_factor: 1.0,
height: 30, height: 30,
width: 0,
exclusive_zone: -1, exclusive_zone: -1,
} }
} }

View file

@ -106,7 +106,6 @@ impl WindowState {
viewport, viewport,
fractional_scale, fractional_scale,
height: builder.height, height: builder.height,
exclusive_zone: builder.exclusive_zone,
size, size,
}); });
@ -143,10 +142,14 @@ impl WindowState {
Rc::clone(self.rendering.window()) Rc::clone(self.rendering.window())
} }
pub(crate) fn layer_surface(&self) -> Rc<ZwlrLayerSurfaceV1> { pub fn layer_surface(&self) -> Rc<ZwlrLayerSurfaceV1> {
self.rendering.layer_surface() self.rendering.layer_surface()
} }
pub fn commit_surface(&self) {
self.rendering.commit_surface();
}
pub fn height(&self) -> u32 { pub fn height(&self) -> u32 {
self.rendering.height() self.rendering.height()
} }

View file

@ -24,7 +24,6 @@ pub struct WindowRendererParams<W: RenderableWindow> {
pub viewport: Option<ManagedWpViewport>, pub viewport: Option<ManagedWpViewport>,
pub fractional_scale: Option<ManagedWpFractionalScaleV1>, pub fractional_scale: Option<ManagedWpFractionalScaleV1>,
pub height: u32, pub height: u32,
pub exclusive_zone: i32,
pub size: PhysicalSize, pub size: PhysicalSize,
} }
@ -35,7 +34,6 @@ pub struct WindowRenderer<W: RenderableWindow> {
viewport: Option<ManagedWpViewport>, viewport: Option<ManagedWpViewport>,
fractional_scale: Option<ManagedWpFractionalScaleV1>, fractional_scale: Option<ManagedWpFractionalScaleV1>,
height: u32, height: u32,
exclusive_zone: i32,
size: PhysicalSize, size: PhysicalSize,
logical_size: PhysicalSize, logical_size: PhysicalSize,
} }
@ -50,7 +48,6 @@ impl<W: RenderableWindow> WindowRenderer<W> {
viewport: params.viewport, viewport: params.viewport,
fractional_scale: params.fractional_scale, fractional_scale: params.fractional_scale,
height: params.height, height: params.height,
exclusive_zone: params.exclusive_zone,
size: params.size, size: params.size,
logical_size: PhysicalSize::default(), logical_size: PhysicalSize::default(),
} }
@ -137,9 +134,6 @@ impl<W: RenderableWindow> WindowRenderer<W> {
} }
} }
self.layer_surface
.set_size(dimensions.logical_width(), dimensions.logical_height());
self.layer_surface.set_exclusive_zone(self.exclusive_zone);
self.surface.commit(); self.surface.commit();
} }
@ -191,4 +185,8 @@ impl<W: RenderableWindow> WindowRenderer<W> {
pub const fn fractional_scale(&self) -> Option<&ManagedWpFractionalScaleV1> { pub const fn fractional_scale(&self) -> Option<&ManagedWpFractionalScaleV1> {
self.fractional_scale.as_ref() self.fractional_scale.as_ref()
} }
pub fn commit_surface(&self) {
self.surface.commit();
}
} }

View file

@ -4,7 +4,7 @@ use layer_shika_adapters::platform::slint_interpreter::{CompilationResult, Compi
use layer_shika_domain::errors::DomainError; use layer_shika_domain::errors::DomainError;
use layer_shika_domain::prelude::{ use layer_shika_domain::prelude::{
AnchorEdges, KeyboardInteractivity, Layer, Margins, OutputPolicy, ScaleFactor, WindowConfig, AnchorEdges, KeyboardInteractivity, Layer, Margins, OutputPolicy, ScaleFactor, WindowConfig,
WindowHeight, WindowDimension,
}; };
use spin_on::spin_on; use spin_on::spin_on;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -136,9 +136,21 @@ impl LayerShika<NeedsComponent> {
} }
impl LayerShika<HasComponent> { impl LayerShika<HasComponent> {
#[must_use]
pub fn dimensions(mut self, width: u32, height: u32) -> Self {
self.config.dimensions = WindowDimension::new(width, height);
self
}
#[must_use] #[must_use]
pub fn height(mut self, height: u32) -> Self { 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 self
} }

View file

@ -2,6 +2,8 @@
mod builder; mod builder;
mod popup_builder; mod popup_builder;
mod shell;
mod shell_composition;
mod system; mod system;
mod value_conversion; mod value_conversion;
@ -27,6 +29,12 @@ pub use layer_shika_domain::value_objects::popup_request::{
pub use popup_builder::PopupBuilder; pub use popup_builder::PopupBuilder;
pub use system::{App, EventContext, EventLoopHandle, ShellControl}; 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 mod calloop {
pub use layer_shika_adapters::platform::calloop::{ pub use layer_shika_adapters::platform::calloop::{
Generic, Interest, Mode, PostAction, RegistrationToken, TimeoutAction, Timer, channel, Generic, Interest, Mode, PostAction, RegistrationToken, TimeoutAction, Timer, channel,
@ -55,7 +63,16 @@ pub mod prelude {
PopupWindow, Result, ShellControl, 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::calloop::{Generic, Interest, Mode, PostAction, RegistrationToken, Timer};
pub use crate::{slint, slint_interpreter}; pub use crate::{slint, slint_interpreter};
pub use layer_shika_domain::prelude::{Margins, ScaleFactor, WindowConfig, WindowDimension};
pub use layer_shika_adapters::platform::wayland::Anchor;
} }

View file

@ -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<F> 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<RefCell<WindowingSystemFacade>>,
windows: HashMap<String, ShellWindowHandle>,
compilation_result: Rc<CompilationResult>,
popup_command_sender: channel::Sender<PopupCommand>,
}
impl Shell {
pub(crate) fn new(
compilation_result: Rc<CompilationResult>,
definitions: &[ShellWindowDefinition],
) -> Result<Self> {
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<ShellWindowConfig> = 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::<Result<Vec<_>>>()?;
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<CompilationResult>,
component_names: &[String],
) -> Result<Self> {
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<ShellWindowConfig> = 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::<Result<Vec<_>>>()?;
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<H: ShellWindowConfigHandler>(&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<F>(&self, f: F)
where
F: Fn(&ComponentInstance, LayerSurfaceHandle<'_>),
{
self.apply_window_config(&f);
}
fn setup_popup_command_handler(&self, receiver: channel::Channel<PopupCommand>) -> 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<F>(&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<F>(&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<CompilationResult> {
&self.compilation_result
}
}
pub struct ShellEventLoopHandle {
system: Weak<RefCell<WindowingSystemFacade>>,
}
impl ShellEventLoopHandle {
pub fn insert_source<S, F, R>(
&self,
source: S,
mut callback: F,
) -> StdResult<RegistrationToken, Error>
where
S: EventSource<Ret = R> + '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<F>(&self, duration: Duration, mut callback: F) -> Result<RegistrationToken>
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<T, F>(
&self,
mut callback: F,
) -> Result<(RegistrationToken, channel::Sender<T>)>
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<F, T>(
&self,
fd: T,
interest: Interest,
mode: Mode,
mut callback: F,
) -> Result<RegistrationToken>
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<Item = &ComponentInstance> {
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(())
}
}

View file

@ -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<Rc<CompilationResult>>,
shell_windows: Vec<ShellWindowDefinition>,
auto_discover_components: Vec<String>,
}
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<CompilationResult>) -> Self {
self.compilation_result = Some(result);
self
}
pub fn register_shell_window(
mut self,
component_name: impl Into<String>,
config: WindowConfig,
) -> Self {
self.shell_windows.push(ShellWindowDefinition {
component_name: component_name.into(),
config,
});
self
}
pub fn register_shell_windows(mut self, definitions: Vec<ShellWindowDefinition>) -> Self {
self.shell_windows.extend(definitions);
self
}
pub fn auto_discover(mut self, component_names: Vec<impl Into<String>>) -> Self {
self.auto_discover_components = component_names.into_iter().map(Into::into).collect();
self
}
pub fn build(self) -> Result<Shell> {
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()
}
}

View file

@ -46,6 +46,10 @@ pub struct ShellControl {
} }
impl ShellControl { impl ShellControl {
pub fn new(sender: channel::Sender<PopupCommand>) -> Self {
Self { sender }
}
pub fn show_popup(&self, request: &PopupRequest) -> Result<()> { pub fn show_popup(&self, request: &PopupRequest) -> Result<()> {
self.sender self.sender
.send(PopupCommand::Show(request.clone())) .send(PopupCommand::Show(request.clone()))
@ -198,6 +202,12 @@ pub struct EventContext<'a> {
app_state: &'a mut AppState, 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 { fn extract_dimensions_from_callback(args: &[Value]) -> PopupDimensions {
let defaults = PopupDimensions::default(); let defaults = PopupDimensions::default();
PopupDimensions::new( PopupDimensions::new(
@ -289,15 +299,21 @@ impl EventContext<'_> {
req: &PopupRequest, req: &PopupRequest,
resize_control: Option<ShellControl>, resize_control: Option<ShellControl>,
) -> Result<PopupHandle> { ) -> Result<PopupHandle> {
log::info!("show_popup called for component '{}'", req.component);
let compilation_result = self.compilation_result().ok_or_else(|| { let compilation_result = self.compilation_result().ok_or_else(|| {
log::error!("No compilation result available");
Error::Domain(DomainError::Configuration { Error::Domain(DomainError::Configuration {
message: "No compilation result available for popup creation".to_string(), message: "No compilation result available for popup creation".to_string(),
}) })
})?; })?;
log::debug!("Got compilation result, looking for component '{}'", req.component);
let definition = compilation_result let definition = compilation_result
.component(&req.component) .component(&req.component)
.ok_or_else(|| { .ok_or_else(|| {
log::error!("Component '{}' not found in compilation result", req.component);
Error::Domain(DomainError::Configuration { Error::Domain(DomainError::Configuration {
message: format!( message: format!(
"{} component not found in compilation result", "{} component not found in compilation result",
@ -306,10 +322,13 @@ impl EventContext<'_> {
}) })
})?; })?;
log::debug!("Found component definition for '{}'", req.component);
self.close_current_popup()?; self.close_current_popup()?;
let is_using_active = self.app_state.active_output().is_some(); let is_using_active = self.app_state.active_output().is_some();
let active_window = self.active_or_primary_output().ok_or_else(|| { let active_window = self.active_or_primary_output().ok_or_else(|| {
log::error!("No active or primary output available");
Error::Domain(DomainError::Configuration { Error::Domain(DomainError::Configuration {
message: "No active or primary output available".to_string(), message: "No active or primary output available".to_string(),
}) })
@ -356,10 +375,6 @@ impl EventContext<'_> {
popup_key_cell.set(popup_handle.key()); popup_key_cell.set(popup_handle.key());
if let Some(popup_window) = popup_manager.get_popup_window(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); popup_window.set_component_instance(instance);
} else { } else {
return Err(Error::Domain(DomainError::Configuration { return Err(Error::Domain(DomainError::Configuration {
@ -425,6 +440,7 @@ impl EventContext<'_> {
let logical_height = height as i32; let logical_height = height as i32;
popup_manager.update_popup_viewport(handle.key(), logical_width, logical_height); popup_manager.update_popup_viewport(handle.key(), logical_width, logical_height);
popup_manager.commit_popup_surface(handle.key());
log::debug!( log::debug!(
"Updated popup viewport to logical size: {}x{} (from resize to {}x{})", "Updated popup viewport to logical size: {}x{} (from resize to {}x{})",
logical_width, logical_width,

View file

@ -1,6 +1,6 @@
use crate::dimensions::ScaleFactor; use crate::dimensions::ScaleFactor;
use crate::value_objects::anchor::AnchorEdges; 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::keyboard_interactivity::KeyboardInteractivity;
use crate::value_objects::layer::Layer; use crate::value_objects::layer::Layer;
use crate::value_objects::margins::Margins; use crate::value_objects::margins::Margins;
@ -8,7 +8,7 @@ use crate::value_objects::output_policy::OutputPolicy;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct WindowConfig { pub struct WindowConfig {
pub height: WindowHeight, pub dimensions: WindowDimension,
pub margin: Margins, pub margin: Margins,
pub exclusive_zone: i32, pub exclusive_zone: i32,
pub scale_factor: ScaleFactor, pub scale_factor: ScaleFactor,
@ -23,7 +23,7 @@ impl WindowConfig {
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
height: WindowHeight::default(), dimensions: WindowDimension::default(),
margin: Margins::default(), margin: Margins::default(),
exclusive_zone: -1, exclusive_zone: -1,
namespace: "layer-shika".to_owned(), namespace: "layer-shika".to_owned(),

View file

@ -9,7 +9,7 @@ pub use crate::errors::{DomainError, Result};
pub use crate::surface_dimensions::SurfaceDimensions; pub use crate::surface_dimensions::SurfaceDimensions;
pub use crate::value_objects::anchor::AnchorEdges; pub use crate::value_objects::anchor::AnchorEdges;
pub use crate::value_objects::anchor_strategy::AnchorStrategy; 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::keyboard_interactivity::KeyboardInteractivity;
pub use crate::value_objects::layer::Layer; pub use crate::value_objects::layer::Layer;
pub use crate::value_objects::margins::Margins; pub use crate::value_objects::margins::Margins;

View file

@ -32,6 +32,16 @@ impl AnchorEdges {
Self(Self::BOTTOM | Self::LEFT | Self::RIGHT) 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] #[must_use]
pub const fn with_top(mut self) -> Self { pub const fn with_top(mut self) -> Self {
self.0 |= Self::TOP; self.0 |= Self::TOP;

View file

@ -1,35 +1,45 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WindowHeight(u32); pub struct WindowDimension {
width: u32,
height: u32,
}
impl WindowHeight { impl WindowDimension {
pub fn new(height: u32) -> Self { pub fn new(width: u32, height: u32) -> Self {
if height == 0 { Self {
Self::default() width: if width == 0 {
Self::default().width
} else { } else {
Self(height) width
},
height: if height == 0 {
Self::default().height
} else {
height
},
} }
} }
pub const fn from_raw(height: u32) -> Self { pub const fn from_raw(width: u32, height: u32) -> Self {
Self(height) Self { width, height }
} }
pub const fn value(&self) -> u32 { pub const fn width(&self) -> u32 {
self.0 self.width
}
pub const fn height(&self) -> u32 {
self.height
} }
} }
impl Default for WindowHeight { impl Default for WindowDimension {
fn default() -> Self { fn default() -> Self {
Self(30) Self {
width: 20,
height: 20,
} }
} }
impl From<u32> for WindowHeight {
fn from(height: u32) -> Self {
Self::new(height)
}
} }
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]

View file

@ -30,6 +30,27 @@
//! # Ok::<(), layer_shika::Error>(()) //! # 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 //! # Re-exports
//! //!
//! This crate re-exports commonly needed types from its dependencies: //! This crate re-exports commonly needed types from its dependencies:
@ -48,6 +69,11 @@ pub use layer_shika_composition::{
Result, ShellControl, Result, ShellControl,
}; };
pub use layer_shika_composition::{
LayerSurfaceHandle, Shell, ShellComposition, ShellEventContext, ShellEventLoopHandle,
ShellWindowConfigHandler, ShellWindowDefinition, ShellWindowHandle,
};
pub use layer_shika_composition::{slint, slint_interpreter}; pub use layer_shika_composition::{slint, slint_interpreter};
/// Re-exported calloop types for event loop integration /// Re-exported calloop types for event loop integration

View file

@ -8,20 +8,25 @@
#![allow(clippy::pub_use)] #![allow(clippy::pub_use)]
// Core API types
pub use crate::{ pub use crate::{
App, Error, EventContext, EventLoopHandle, LayerShika, PopupWindow, Result, ShellControl, 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::{ pub use crate::{
AnchorEdges, KeyboardInteractivity, Layer, OutputGeometry, OutputHandle, OutputInfo, AnchorEdges, KeyboardInteractivity, Layer, OutputGeometry, OutputHandle, OutputInfo,
OutputPolicy, OutputRegistry, PopupHandle, PopupPlacement, PopupPositioningMode, PopupRequest, OutputPolicy, OutputRegistry, PopupHandle, PopupPlacement, PopupPositioningMode, PopupRequest,
PopupSize, PopupSize,
}; };
// Event loop types pub use layer_shika_composition::prelude::{
Anchor, Margins, ScaleFactor, WindowConfig, WindowDimension,
};
pub use crate::calloop; pub use crate::calloop;
// UI framework re-exports
pub use crate::{slint, slint_interpreter}; pub use crate::{slint, slint_interpreter};