mirror of
https://codeberg.org/waydeer/layer-shika.git
synced 2025-12-12 14:25:54 +00:00
feat: add event loop integration examples
This commit is contained in:
parent
26a994a4b8
commit
8c9e5fb92c
9 changed files with 565 additions and 16 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
|
@ -150,6 +150,12 @@ dependencies = [
|
|||
"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]]
|
||||
name = "arrayref"
|
||||
version = "0.3.8"
|
||||
|
|
@ -1007,6 +1013,16 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-loop-examples"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"env_logger",
|
||||
"layer-shika",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.0"
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ members = [
|
|||
"examples/simple-bar",
|
||||
"examples/multi-surface",
|
||||
"examples/declarative-config",
|
||||
"examples/event-loop",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
|
|
|||
|
|
@ -20,22 +20,6 @@ cd examples/simple-bar
|
|||
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
|
||||
|
||||
**Recommended learning path:**
|
||||
|
|
@ -43,6 +27,7 @@ cargo build -p declarative-config
|
|||
1. **simple-bar** - Start here to understand the basics
|
||||
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
|
||||
|
||||
## Common Patterns
|
||||
|
||||
|
|
|
|||
27
examples/event-loop/Cargo.toml
Normal file
27
examples/event-loop/Cargo.toml
Normal 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
|
||||
72
examples/event-loop/README.md
Normal file
72
examples/event-loop/README.md
Normal 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
|
||||
})?;
|
||||
```
|
||||
158
examples/event-loop/src/channel.rs
Normal file
158
examples/event-loop/src/channel.rs
Normal 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}")
|
||||
}
|
||||
140
examples/event-loop/src/custom_source.rs
Normal file
140
examples/event-loop/src/custom_source.rs
Normal 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}")
|
||||
}
|
||||
83
examples/event-loop/src/timer.rs
Normal file
83
examples/event-loop/src/timer.rs
Normal 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}")
|
||||
}
|
||||
67
examples/event-loop/ui/demo.slint
Normal file
67
examples/event-loop/ui/demo.slint
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue