feat: add surface config runtime maniputation example

This commit is contained in:
drendog 2025-12-08 01:13:01 +01:00
parent c2064c6012
commit da4871e4b9
Signed by: dwenya
GPG key ID: 8DD77074645332D0
7 changed files with 441 additions and 4 deletions

9
Cargo.lock generated
View file

@ -3092,6 +3092,15 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "runtime-surface-config"
version = "0.1.0"
dependencies = [
"env_logger",
"layer-shika",
"log",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"

View file

@ -26,6 +26,7 @@ members = [
"examples/multi-surface", "examples/multi-surface",
"examples/declarative-config", "examples/declarative-config",
"examples/event-loop", "examples/event-loop",
"examples/runtime-surface-config",
] ]
[workspace.package] [workspace.package]

View file

@ -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 simple-bar
cargo run -p multi-surface cargo run -p multi-surface
cargo run -p declarative-config cargo run -p declarative-config
cargo run -p runtime-surface-config
# Or with logging
RUST_LOG=info cargo run -p simple-bar
# Or from the example directory # Or from the example directory
cd examples/simple-bar cd examples/simple-bar
@ -28,6 +26,7 @@ cargo run
2. **multi-surface** - Learn about multiple surfaces and callbacks 2. **multi-surface** - Learn about multiple surfaces and callbacks
3. **declarative-config** - See the alternative configuration approach 3. **declarative-config** - See the alternative configuration approach
4. **event-loop** - Explore event loop integration with timers and channels 4. **event-loop** - Explore event loop integration with timers and channels
5. **runtime-surface-config** - Surface configuration manipulation at runtime
## Common Patterns ## Common Patterns
@ -46,7 +45,7 @@ All examples use layer-shika's `Result<()>` type for error handling with the `?`
## Coming Soon ## Coming Soon
Additional examples demonstrating: Additional examples demonstrating:
- Event loop integration (timers, channels, custom event sources)
- Multi-output support (multiple monitors) with different surfaces per output - Multi-output support (multiple monitors) with different surfaces per output
- Advanced popup patterns - Advanced popup patterns
- Dynamic UI loading - Dynamic UI loading

View file

@ -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

View file

@ -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()`

View file

@ -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<Sender<UiUpdate>>,
shell: &Shell,
state: &Rc<RefCell<BarState>>,
) -> 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<Sender<UiUpdate>>,
shell: &Shell,
state: &Rc<RefCell<BarState>>,
) -> 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<Sender<UiUpdate>>,
shell: &Shell,
state: &Rc<RefCell<BarState>>,
) -> 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(())
}

View file

@ -0,0 +1,58 @@
// Runtime Control Example - Interactive Status Bar
import { Button } from "std-widgets.slint";
export component Bar inherits Window {
in-out property <bool> is-expanded: false;
in-out property <string> current-anchor: "Top";
in-out property <string> 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;
}
}
}
}