diff --git a/Cargo.lock b/Cargo.lock index 64788ba..5c61690 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3092,6 +3092,15 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "runtime-surface-config" +version = "0.1.0" +dependencies = [ + "env_logger", + "layer-shika", + "log", +] + [[package]] name = "rustc-hash" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 4a4ecb2..4e6f5c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "examples/multi-surface", "examples/declarative-config", "examples/event-loop", + "examples/runtime-surface-config", ] [workspace.package] diff --git a/examples/README.md b/examples/README.md index 9c91522..daa953a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,9 +11,7 @@ Each example is a standalone crate that can be run from anywhere in the workspac cargo run -p simple-bar cargo run -p multi-surface cargo run -p declarative-config - -# Or with logging -RUST_LOG=info cargo run -p simple-bar +cargo run -p runtime-surface-config # Or from the example directory cd examples/simple-bar @@ -28,6 +26,7 @@ cargo run 2. **multi-surface** - Learn about multiple surfaces and callbacks 3. **declarative-config** - See the alternative configuration approach 4. **event-loop** - Explore event loop integration with timers and channels +5. **runtime-surface-config** - Surface configuration manipulation at runtime ## Common Patterns @@ -46,7 +45,7 @@ All examples use layer-shika's `Result<()>` type for error handling with the `?` ## Coming Soon Additional examples demonstrating: -- Event loop integration (timers, channels, custom event sources) + - Multi-output support (multiple monitors) with different surfaces per output - Advanced popup patterns - Dynamic UI loading diff --git a/examples/runtime-surface-config/Cargo.toml b/examples/runtime-surface-config/Cargo.toml new file mode 100644 index 0000000..3b841a3 --- /dev/null +++ b/examples/runtime-surface-config/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "runtime-surface-config" +version.workspace = true +edition.workspace = true +license.workspace = true +publish = false + +[lints] +workspace = true + +[dependencies] +layer-shika = { path = "../.." } +env_logger = "0.11.7" +log.workspace = true diff --git a/examples/runtime-surface-config/README.md b/examples/runtime-surface-config/README.md new file mode 100644 index 0000000..273c01c --- /dev/null +++ b/examples/runtime-surface-config/README.md @@ -0,0 +1,69 @@ +# Runtime Surface Config Example + +This example demonstrates layer-shika's runtime surface configuration capabilities using the Surface Control API. + +## Features Demonstrated + +1. **Dynamic Sizing**: Toggle between compact (32px) and expanded (64px) bar heights +2. **Anchor Position Control**: Switch between top and bottom screen edges at runtime +3. **Layer Management**: Cycle through Background, Bottom, Top, and Overlay layers +4. **Channel-based UI Updates**: Use event loop channels to update UI state from callbacks +5. **Surface Control API**: Manipulate surfaces via callback handlers + +## Controls + +- **Expand/Collapse Button**: Toggle between 32px and 64px bar heights +- **Switch Anchor**: Toggle between top and bottom screen positions +- **Switch Layer**: Cycle through Background → Bottom → Top → Overlay layers + +## Running the Example + +```bash +cargo run -p runtime-surface-config +``` + +## Implementation Highlights + +### Control from Slint Callbacks + +```rust +shell.on("Bar", "toggle-size", move |control| { + let bar = control.surface("Bar"); + bar.resize(width, height)?; + bar.set_exclusive_zone(new_size)?; + Value::Struct(Struct::from_iter([("expanded".into(), is_expanded.into())])) +})?; +``` + +### Channel-based UI Updates + +```rust +let (_token, sender) = handle.add_channel(|message: UiUpdate, app_state| { + for surface in app_state.all_outputs() { + let component = surface.component_instance(); + match &message { + UiUpdate::IsExpanded(is_expanded) => { + component.set_property("is-expanded", (*is_expanded).into())?; + } + // ... other updates + } + } +})?; +``` + +## API Patterns + +This example showcases the Surface Control API pattern: + +**SurfaceControlHandle** (channel-based): + +- Accessible via `control.surface(name)` in callback handlers +- Safe to call from Slint callbacks +- Commands execute asynchronously in event loop + +**Event Loop Channels**: + +- Use `add_channel` to create message handlers +- Send messages from callbacks to update UI state +- Process messages in event loop context +- Access all surfaces via `app_state.all_outputs()` diff --git a/examples/runtime-surface-config/src/main.rs b/examples/runtime-surface-config/src/main.rs new file mode 100644 index 0000000..0dcad55 --- /dev/null +++ b/examples/runtime-surface-config/src/main.rs @@ -0,0 +1,287 @@ +use layer_shika::calloop::channel::Sender; +use layer_shika::prelude::*; +use layer_shika::slint::SharedString; +use layer_shika::slint_interpreter::{Struct, Value}; +use std::cell::RefCell; +use std::path::PathBuf; +use std::rc::Rc; + +#[derive(Debug)] +enum UiUpdate { + IsExpanded(bool), + CurrentAnchor(String), + CurrentLayer(String), +} +enum AnchorPosition { + Top, + Bottom, +} + +struct BarState { + is_expanded: bool, + current_anchor: AnchorPosition, + current_layer: Layer, +} + +impl BarState { + fn new() -> Self { + Self { + is_expanded: false, + current_anchor: AnchorPosition::Top, + current_layer: Layer::Top, + } + } + + fn anchor_name(&self) -> &'static str { + match self.current_anchor { + AnchorPosition::Top => "Top", + AnchorPosition::Bottom => "Bottom", + } + } + + fn next_anchor(&mut self) { + self.current_anchor = match self.current_anchor { + AnchorPosition::Top => AnchorPosition::Bottom, + AnchorPosition::Bottom => AnchorPosition::Top, + }; + } + + fn get_anchor_edges(&self) -> AnchorEdges { + match self.current_anchor { + AnchorPosition::Top => AnchorEdges::top_bar(), + AnchorPosition::Bottom => AnchorEdges::bottom_bar(), + } + } + + fn layer_name(&self) -> &'static str { + match self.current_layer { + Layer::Background => "Background", + Layer::Bottom => "Bottom", + Layer::Top => "Top", + Layer::Overlay => "Overlay", + } + } + + fn next_layer(&mut self) -> Layer { + self.current_layer = match self.current_layer { + Layer::Background => Layer::Bottom, + Layer::Bottom => Layer::Top, + Layer::Top => Layer::Overlay, + Layer::Overlay => Layer::Background, + }; + self.current_layer + } +} + +fn setup_toggle_size_callback( + sender: &Rc>, + shell: &Shell, + state: &Rc>, +) -> Result<()> { + let state_clone = Rc::clone(state); + let sender_clone = Rc::clone(sender); + shell.on("Bar", "toggle-size", move |control| { + let is_expanded = { + let mut st = state_clone.borrow_mut(); + st.is_expanded = !st.is_expanded; + + let new_size = if st.is_expanded { 64 } else { 32 }; + + let (width, height) = match st.current_anchor { + AnchorPosition::Top | AnchorPosition::Bottom => { + log::info!("Resizing horizontal bar to {}px", new_size); + (0, new_size) + } + }; + + let bar = control.surface("Bar"); + if let Err(e) = bar.resize(width, height) { + log::error!("Failed to resize bar: {}", e); + } + + if let Err(e) = bar.set_exclusive_zone(new_size.try_into().unwrap_or(32)) { + log::error!("Failed to set exclusive zone: {}", e); + } + + if let Err(e) = control.surface("Bar").set_margins((0, 0, 0, 0)) { + log::error!("Failed to set margins: {}", e); + } + + log::info!( + "Updated bar state: size={}, is_expanded={}", + new_size, + st.is_expanded + ); + + st.is_expanded + }; + + if let Err(e) = sender_clone.send(UiUpdate::IsExpanded(is_expanded)) { + log::error!("Failed to send UI update: {}", e); + } + + if let Err(e) = control.request_redraw() { + log::error!("Failed to request redraw: {}", e); + } + + Value::Struct(Struct::from_iter([("expanded".into(), is_expanded.into())])) + }) +} + +fn setup_anchor_switch_callback( + sender: &Rc>, + shell: &Shell, + state: &Rc>, +) -> Result<()> { + let state_clone = Rc::clone(state); + let sender_clone = Rc::clone(sender); + shell.on("Bar", "switch-anchor", move |control| { + let anchor_name = { + let mut st = state_clone.borrow_mut(); + st.next_anchor(); + + log::info!("Switching to anchor: {}", st.anchor_name()); + + let bar = control.surface("Bar"); + if let Err(e) = bar.set_anchor(st.get_anchor_edges()) { + log::error!("Failed to apply config: {}", e); + } + + st.anchor_name() + }; + + if let Err(e) = sender_clone.send(UiUpdate::CurrentAnchor(anchor_name.to_string())) { + log::error!("Failed to send UI update: {}", e); + } + + if let Err(e) = control.request_redraw() { + log::error!("Failed to request redraw: {}", e); + } + + log::info!("Switched to {} anchor", anchor_name); + + Value::Struct(Struct::from_iter([( + "anchor".into(), + SharedString::from(anchor_name).into(), + )])) + }) +} + +fn setup_layer_switch_callback( + sender: &Rc>, + shell: &Shell, + state: &Rc>, +) -> Result<()> { + let state_clone = Rc::clone(state); + let sender_clone = Rc::clone(sender); + shell.on("Bar", "switch-layer", move |control| { + let layer_name = { + let mut st = state_clone.borrow_mut(); + let new_layer = st.next_layer(); + + log::info!("Switching to layer: {:?}", new_layer); + + let bar = control.surface("Bar"); + if let Err(e) = bar.set_layer(new_layer) { + log::error!("Failed to set layer: {}", e); + } + + st.layer_name() + }; + + if let Err(e) = sender_clone.send(UiUpdate::CurrentLayer(layer_name.to_string())) { + log::error!("Failed to send UI update: {}", e); + } + + log::info!("Switched to {} layer", layer_name); + + Value::Struct(Struct::from_iter([( + "layer".into(), + SharedString::from(layer_name).into(), + )])) + }) +} + +fn main() -> Result<()> { + env_logger::builder() + .filter_level(log::LevelFilter::Debug) + .init(); + + log::info!("Starting runtime-control example"); + log::info!("This example demonstrates dynamic surface manipulation at runtime"); + + let ui_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui/bar.slint"); + + let state = Rc::new(RefCell::new(BarState::new())); + + let mut shell = Shell::from_file(ui_path) + .surface("Bar") + .height(32) + .anchor(AnchorEdges::top_bar()) + .exclusive_zone(32) + .namespace("runtime-control-example") + .build()?; + + shell.with_all_surfaces(|_name, component| { + log::info!("Initializing properties for Bar surface"); + let state_ref = state.borrow(); + + let set_property = |name: &str, value: Value| { + if let Err(e) = component.set_property(name, value) { + log::error!("Failed to set initial {}: {}", name, e); + } + }; + + set_property("is-expanded", state_ref.is_expanded.into()); + set_property( + "current-anchor", + SharedString::from(state_ref.anchor_name()).into(), + ); + set_property( + "current-layer", + SharedString::from(state_ref.layer_name()).into(), + ); + + log::info!("Initialized properties for Bar surface"); + }); + + let handle = shell.event_loop_handle(); + let (_token, sender) = handle.add_channel(|message: UiUpdate, app_state| { + log::info!("Received UI update: {:?}", message); + + for surface in app_state.all_outputs() { + let component = surface.component_instance(); + + match &message { + UiUpdate::IsExpanded(is_expanded) => { + if let Err(e) = component.set_property("is-expanded", (*is_expanded).into()) { + log::error!("Failed to set is-expanded: {}", e); + } + } + UiUpdate::CurrentAnchor(anchor) => { + if let Err(e) = component + .set_property("current-anchor", SharedString::from(anchor.as_str()).into()) + { + log::error!("Failed to set current-anchor: {}", e); + } + } + UiUpdate::CurrentLayer(layer) => { + if let Err(e) = component + .set_property("current-layer", SharedString::from(layer.as_str()).into()) + { + log::error!("Failed to set current-layer: {}", e); + } + } + } + } + })?; + + let sender_rc = Rc::new(sender); + + setup_toggle_size_callback(&sender_rc, &shell, &state)?; + setup_anchor_switch_callback(&sender_rc, &shell, &state)?; + setup_layer_switch_callback(&sender_rc, &shell, &state)?; + shell.run()?; + + Ok(()) +} diff --git a/examples/runtime-surface-config/ui/bar.slint b/examples/runtime-surface-config/ui/bar.slint new file mode 100644 index 0000000..24771fa --- /dev/null +++ b/examples/runtime-surface-config/ui/bar.slint @@ -0,0 +1,58 @@ +// Runtime Control Example - Interactive Status Bar + +import { Button } from "std-widgets.slint"; +export component Bar inherits Window { + in-out property is-expanded: false; + in-out property current-anchor: "Top"; + in-out property current-layer: "Top"; + + callback toggle-size() -> {expanded: bool}; + callback switch-anchor() -> {anchor: string}; + callback switch-layer() -> {layer: string}; + + HorizontalLayout { + spacing: 12px; + + Text { + text: "Click buttons to control surface"; + vertical-alignment: center; + } + + Text { + text: "Anchor: " + current-anchor; + font-size: 12px; + vertical-alignment: center; + } + + Text { + text: "Layer: " + current-layer; + font-size: 12px; + vertical-alignment: center; + } + + Rectangle { + horizontal-stretch: 1; + } + + Button { + text: is-expanded ? "Collapse" : "Expand"; + clicked => { + root.is-expanded = toggle-size().expanded; + } + } + + Button { + text: "Switch Anchor"; + clicked => { + root.current-anchor = switch-anchor().anchor; + } + } + + Button { + text: "Switch Layer"; + clicked => { + root.current-layer = switch-layer().layer; + } + } + } +}