diff --git a/Cargo.lock b/Cargo.lock index 507f93c..64788ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index b8805ad..4a4ecb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "examples/simple-bar", "examples/multi-surface", "examples/declarative-config", + "examples/event-loop", ] [workspace.package] diff --git a/examples/README.md b/examples/README.md index a4e17f2..9c91522 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/examples/event-loop/Cargo.toml b/examples/event-loop/Cargo.toml new file mode 100644 index 0000000..49ab409 --- /dev/null +++ b/examples/event-loop/Cargo.toml @@ -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 diff --git a/examples/event-loop/README.md b/examples/event-loop/README.md new file mode 100644 index 0000000..846db4a --- /dev/null +++ b/examples/event-loop/README.md @@ -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 +})?; +``` diff --git a/examples/event-loop/src/channel.rs b/examples/event-loop/src/channel.rs new file mode 100644 index 0000000..bf6ab58 --- /dev/null +++ b/examples/event-loop/src/channel.rs @@ -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) { + 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) { + 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}") +} diff --git a/examples/event-loop/src/custom_source.rs b/examples/event-loop/src/custom_source.rs new file mode 100644 index 0000000..f5a8611 --- /dev/null +++ b/examples/event-loop/src/custom_source.rs @@ -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, +} + +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}") +} diff --git a/examples/event-loop/src/timer.rs b/examples/event-loop/src/timer.rs new file mode 100644 index 0000000..d45643d --- /dev/null +++ b/examples/event-loop/src/timer.rs @@ -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}") +} diff --git a/examples/event-loop/ui/demo.slint b/examples/event-loop/ui/demo.slint new file mode 100644 index 0000000..e262d39 --- /dev/null +++ b/examples/event-loop/ui/demo.slint @@ -0,0 +1,67 @@ + +export component Main inherits Window { + in property time: "00:00:00"; + in property counter: 0; + in property 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; + } + } + } + } +}