feat: add event loop integration examples

This commit is contained in:
drendog 2025-12-06 02:25:42 +01:00
parent 26a994a4b8
commit 8c9e5fb92c
Signed by: dwenya
GPG key ID: 8DD77074645332D0
9 changed files with 565 additions and 16 deletions

16
Cargo.lock generated
View file

@ -150,6 +150,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]] [[package]]
name = "arrayref" name = "arrayref"
version = "0.3.8" version = "0.3.8"
@ -1007,6 +1013,16 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "event-loop-examples"
version = "0.1.0"
dependencies = [
"anyhow",
"env_logger",
"layer-shika",
"log",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.1.0" version = "2.1.0"

View file

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

View file

@ -20,22 +20,6 @@ cd examples/simple-bar
cargo run cargo run
``` ```
## Building All Examples
From the workspace root:
```bash
cargo build --workspace
```
Or build a specific example:
```bash
cargo build -p simple-bar
cargo build -p multi-surface
cargo build -p declarative-config
```
## Example Progression ## Example Progression
**Recommended learning path:** **Recommended learning path:**
@ -43,6 +27,7 @@ cargo build -p declarative-config
1. **simple-bar** - Start here to understand the basics 1. **simple-bar** - Start here to understand the basics
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
## Common Patterns ## Common Patterns

View file

@ -0,0 +1,27 @@
[package]
name = "event-loop-examples"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[lints]
workspace = true
[[bin]]
name = "timer"
path = "src/timer.rs"
[[bin]]
name = "channel"
path = "src/channel.rs"
[[bin]]
name = "custom-source"
path = "src/custom_source.rs"
[dependencies]
layer-shika = { path = "../.." }
anyhow = "1.0"
env_logger = "0.11.7"
log.workspace = true

View file

@ -0,0 +1,72 @@
# Event Loop Integration Examples
This directory contains examples demonstrating how to integrate custom event sources
with layer-shika's event loop.
## Examples
### Timer (`timer.rs`)
Demonstrates how to add periodic timers to update UI elements (e.g., a clock).
```bash
cargo run --bin timer
```
### Channel (`channel.rs`)
Shows how to use channels for communication between background threads and the UI.
Useful for async operations, network requests, or any off-main-thread work.
```bash
cargo run --bin channel
```
### Custom Event Source (`custom_source.rs`)
Demonstrates adding custom file descriptor-based event sources for I/O monitoring.
```bash
cargo run --bin custom-source
```
## Key Concepts
All examples use `shell.event_loop_handle()` to get a handle that allows registering
event sources with the main event loop. The callbacks receive `&mut AppState` which
provides access to window components and output information.
### Timer Pattern
```rust
let handle = shell.event_loop_handle();
handle.add_timer(Duration::from_secs(1), |_instant, app_state| {
// Update UI components here
TimeoutAction::ToInstant(Instant::now() + Duration::from_secs(1))
})?;
```
### Channel Pattern
```rust
let handle = shell.event_loop_handle();
let (_token, sender) = handle.add_channel(|message: MyMessage, app_state| {
// Handle messages from background threads
})?;
// Send from another thread
std::thread::spawn(move || {
sender.send(MyMessage::Update("data".into())).unwrap();
});
```
### File Descriptor Pattern
```rust
use layer_shika::calloop::{Generic, Interest, Mode};
let handle = shell.event_loop_handle();
handle.add_fd(file, Interest::READ, Mode::Level, |app_state| {
// Handle I/O readiness
})?;
```

View file

@ -0,0 +1,158 @@
use std::path::PathBuf;
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use anyhow::Result;
use layer_shika::calloop::TimeoutAction;
use layer_shika::calloop::channel::Sender;
use layer_shika::prelude::*;
use layer_shika::slint_interpreter::Value;
enum UiMessage {
UpdateStatus(String),
IncrementCounter(i32),
BackgroundTaskComplete(String),
}
fn main() -> Result<()> {
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();
log::info!("Starting channel example");
let ui_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui/demo.slint");
let mut shell = Shell::from_file(&ui_path)
.surface("Main")
.size(400, 200)
.layer(Layer::Top)
.namespace("channel-example")
.build()?;
let handle = shell.event_loop_handle();
let (_token, sender) = handle.add_channel(|message: UiMessage, app_state| {
for window in app_state.all_outputs() {
let component = window.component_instance();
match &message {
UiMessage::UpdateStatus(status) => {
if let Err(e) =
component.set_property("status", Value::String(status.clone().into()))
{
log::error!("Failed to set status: {e}");
}
log::info!("Status updated: {}", status);
}
UiMessage::IncrementCounter(delta) => {
if let Ok(Value::Number(current)) = component.get_property("counter") {
#[allow(clippy::cast_possible_truncation)]
let new_value = current as i32 + delta;
if let Err(e) =
component.set_property("counter", Value::Number(f64::from(new_value)))
{
log::error!("Failed to set counter: {e}");
}
log::debug!("Counter: {}", new_value);
}
}
UiMessage::BackgroundTaskComplete(result) => {
if let Err(e) = component
.set_property("status", Value::String(format!("Done: {result}").into()))
{
log::error!("Failed to set status: {e}");
}
log::info!("Background task complete: {}", result);
}
}
}
})?;
handle.add_timer(Duration::from_secs(1), |_instant, app_state| {
let time_str = current_time_string();
for window in app_state.all_outputs() {
if let Err(e) = window
.component_instance()
.set_property("time", Value::String(time_str.clone().into()))
{
log::error!("Failed to set time property: {e}");
}
}
TimeoutAction::ToInstant(Instant::now() + Duration::from_secs(1))
})?;
spawn_background_worker(sender.clone());
spawn_counter_worker(sender);
shell.run()?;
Ok(())
}
fn spawn_background_worker(sender: Sender<UiMessage>) {
thread::spawn(move || {
let tasks = vec![
("Loading configuration...", 500),
("Connecting to services...", 800),
("Fetching data...", 1200),
("Processing results...", 600),
];
for (status, delay_ms) in tasks {
if sender
.send(UiMessage::UpdateStatus(status.to_string()))
.is_err()
{
return;
}
thread::sleep(Duration::from_millis(delay_ms));
}
if sender
.send(UiMessage::BackgroundTaskComplete(
"All tasks finished".to_string(),
))
.is_err()
{
return;
}
loop {
thread::sleep(Duration::from_secs(5));
if sender
.send(UiMessage::UpdateStatus(
"Heartbeat from background".to_string(),
))
.is_err()
{
break;
}
}
});
}
fn spawn_counter_worker(sender: Sender<UiMessage>) {
thread::spawn(move || {
loop {
thread::sleep(Duration::from_millis(50));
if sender.send(UiMessage::IncrementCounter(1)).is_err() {
break;
}
}
});
}
fn current_time_string() -> String {
let now = SystemTime::now();
let duration = now.duration_since(UNIX_EPOCH).unwrap_or_default();
let secs = duration.as_secs();
let hours = (secs / 3600) % 24;
let minutes = (secs / 60) % 60;
let seconds = secs % 60;
format!("{hours:02}:{minutes:02}:{seconds:02}")
}

View file

@ -0,0 +1,140 @@
use std::cell::Cell;
use std::fs::File;
use std::io::{BufReader, Write};
use std::os::unix::io::{AsFd, BorrowedFd, FromRawFd, IntoRawFd};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use anyhow::Result;
use layer_shika::calloop::{Interest, Mode, TimeoutAction};
use layer_shika::prelude::*;
use layer_shika::slint_interpreter::Value;
struct ReadablePipe {
reader: BufReader<File>,
}
impl AsFd for ReadablePipe {
fn as_fd(&self) -> BorrowedFd<'_> {
self.reader.get_ref().as_fd()
}
}
fn main() -> Result<()> {
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();
log::info!("Starting custom event source example");
let ui_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui/demo.slint");
let mut shell = Shell::from_file(&ui_path)
.surface("Main")
.size(400, 200)
.layer(Layer::Top)
.namespace("custom-source-example")
.build()?;
let (mut write_end, read_end) = create_pipe()?;
let readable = ReadablePipe {
reader: BufReader::new(read_end),
};
let handle = shell.event_loop_handle();
let counter = Cell::new(0i32);
handle.add_fd(readable, Interest::READ, Mode::Level, move |app_state| {
log::debug!("Pipe readable event triggered");
let count = counter.get() + 1;
counter.set(count);
let status_text = format!("Events received: {count}");
for window in app_state.all_outputs() {
let component = window.component_instance();
if let Err(e) = component.set_property("counter", Value::Number(f64::from(count))) {
log::error!("Failed to set counter: {e}");
}
if let Err(e) =
component.set_property("status", Value::String(status_text.clone().into()))
{
log::error!("Failed to set status: {e}");
}
}
})?;
thread::spawn(move || {
let mut event_num = 0;
loop {
thread::sleep(Duration::from_millis(500));
event_num += 1;
let message = format!("event-{event_num}\n");
if write_end.write_all(message.as_bytes()).is_err() {
break;
}
if write_end.flush().is_err() {
break;
}
log::debug!("Wrote event {} to pipe", event_num);
}
});
handle.add_timer(Duration::from_secs(1), |_instant, app_state| {
let time_str = current_time_string();
for window in app_state.all_outputs() {
if let Err(e) = window
.component_instance()
.set_property("time", Value::String(time_str.clone().into()))
{
log::error!("Failed to set time property: {e}");
}
}
TimeoutAction::ToInstant(Instant::now() + Duration::from_secs(1))
})?;
shell.with_surface("Main", |component| {
if let Err(e) =
component.set_property("status", Value::String("Waiting for pipe events...".into()))
{
log::error!("Failed to set status: {e}");
}
})?;
shell.run()?;
Ok(())
}
fn create_pipe() -> Result<(File, File)> {
let (read_stream, write_stream) = UnixStream::pair()?;
read_stream.set_nonblocking(true)?;
write_stream.set_nonblocking(true)?;
Ok(unsafe {
(
FromRawFd::from_raw_fd(write_stream.into_raw_fd()),
FromRawFd::from_raw_fd(read_stream.into_raw_fd()),
)
})
}
fn current_time_string() -> String {
let now = SystemTime::now();
let duration = now.duration_since(UNIX_EPOCH).unwrap_or_default();
let secs = duration.as_secs();
let hours = (secs / 3600) % 24;
let minutes = (secs / 60) % 60;
let seconds = secs % 60;
format!("{hours:02}:{minutes:02}:{seconds:02}")
}

View file

@ -0,0 +1,83 @@
use std::cell::Cell;
use std::path::PathBuf;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use anyhow::Result;
use layer_shika::calloop::TimeoutAction;
use layer_shika::prelude::*;
use layer_shika::slint_interpreter::Value;
fn main() -> Result<()> {
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();
log::info!("Starting timer example");
let ui_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui/demo.slint");
let mut shell = Shell::from_file(&ui_path)
.surface("Main")
.size(400, 200)
.layer(Layer::Top)
.namespace("timer-example")
.build()?;
let handle = shell.event_loop_handle();
handle.add_timer(Duration::ZERO, |_instant, app_state| {
let time_str = current_time_string();
for window in app_state.all_outputs() {
if let Err(e) = window
.component_instance()
.set_property("time", Value::String(time_str.clone().into()))
{
log::error!("Failed to set time property: {e}");
}
}
log::debug!("Timer tick: {}", time_str);
TimeoutAction::ToInstant(Instant::now() + Duration::from_secs(1))
})?;
let counter = Cell::new(0i32);
handle.add_timer(Duration::ZERO, move |_instant, app_state| {
let count = counter.get() + 1;
counter.set(count);
for window in app_state.all_outputs() {
if let Err(e) = window
.component_instance()
.set_property("counter", Value::Number(f64::from(count)))
{
log::error!("Failed to set counter property: {e}");
}
}
TimeoutAction::ToInstant(Instant::now() + Duration::from_millis(100))
})?;
shell.with_surface("Main", |component| {
if let Err(e) = component.set_property("status", Value::String("Timer running...".into())) {
log::error!("Failed to set status property: {e}");
}
})?;
shell.run()?;
Ok(())
}
fn current_time_string() -> String {
let now = SystemTime::now();
let duration = now.duration_since(UNIX_EPOCH).unwrap_or_default();
let secs = duration.as_secs();
let hours = (secs / 3600) % 24;
let minutes = (secs / 60) % 60;
let seconds = secs % 60;
format!("{hours:02}:{minutes:02}:{seconds:02}")
}

View file

@ -0,0 +1,67 @@
export component Main inherits Window {
in property <string> time: "00:00:00";
in property <int> counter: 0;
in property <string> status: "Waiting...";
background: #2d2d2d;
VerticalLayout {
alignment: center;
padding: 20px;
spacing: 20px;
Text {
text: "Event Loop Demo";
font-size: 24px;
color: #ffffff;
horizontal-alignment: center;
}
Text {
text: status;
font-size: 16px;
color: #ffb74d;
horizontal-alignment: center;
}
HorizontalLayout {
alignment: center;
spacing: 40px;
VerticalLayout {
alignment: center;
Text {
text: "Time";
font-size: 14px;
color: #888888;
horizontal-alignment: center;
}
Text {
text: time;
font-size: 32px;
color: #4fc3f7;
horizontal-alignment: center;
}
}
VerticalLayout {
alignment: center;
Text {
text: "Counter";
font-size: 14px;
color: #888888;
horizontal-alignment: center;
}
Text {
text: counter;
font-size: 32px;
color: #81c784;
horizontal-alignment: center;
}
}
}
}
}