diff --git a/examples/timer/Cargo.toml b/examples/timer/Cargo.toml new file mode 100644 index 00000000..6cd3b0d1 --- /dev/null +++ b/examples/timer/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "timer" +edition = "2021" +license.workspace = true +version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +floem = { path = "../.." } diff --git a/examples/timer/README.md b/examples/timer/README.md new file mode 100644 index 00000000..9c903a0b --- /dev/null +++ b/examples/timer/README.md @@ -0,0 +1,18 @@ +# Timer + +This is an example timer app, as described in +[task 4][task4] of [7gui tasks][7gui]. + +> Timer deals with concurrency in the sense that a timer process +> that updates the elapsed time runs concurrently to the user’s +> interactions with the GUI application. This also means that the +> solution to competing user and signal interactions is tested. The +> fact that slider adjustments must be reflected immediately moreover +> tests the responsiveness of the solution. A good solution will make +> it clear that the signal is a timer tick and, as always, has not +> much scaffolding. + +![timer](https://github.com/lapce/floem/assets/23398472/b55dae4f-56fe-4e9f-a0ee-1898db048588) + +[task4]: https://eugenkiss.github.io/7guis/tasks/#timer +[7gui]: https://eugenkiss.github.io/7guis/ diff --git a/examples/timer/src/main.rs b/examples/timer/src/main.rs new file mode 100644 index 00000000..8366e788 --- /dev/null +++ b/examples/timer/src/main.rs @@ -0,0 +1,91 @@ +use std::time::{Duration, Instant}; + +use floem::{ + action::exec_after, + reactive::{create_effect, create_rw_signal}, + style::BorderRadius, + unit::UnitExt, + view::View, + views::{container, label, stack, text, v_stack, Decorators}, + widgets::{button, slider}, +}; + +fn main() { + floem::launch(app_view); +} + +fn app_view() -> impl View { + // We take maximum duration as 100s for convenience so that + // one percent represents one second. + let target_duration = create_rw_signal(100.0); + let duration_slider = thin_slider(move || target_duration.get()) + .on_change_pct(move |new| target_duration.set(new)); + + let elapsed_time = create_rw_signal(Duration::ZERO); + let is_active = move || elapsed_time.get().as_secs_f32() < target_duration.get(); + + let elapsed_time_label = label(move || { + format!( + "{:.1}s", + if is_active() { + elapsed_time.get().as_secs_f32() + } else { + target_duration.get() + } + ) + }); + + let tick = create_rw_signal(()); + create_effect(move |_| { + tick.track(); + let before_exec = Instant::now(); + + exec_after(Duration::from_millis(100), move |_| { + if is_active() { + elapsed_time.update(|d| *d += before_exec.elapsed()); + } + tick.set(()); + }); + }); + + let progress = move || elapsed_time.get().as_secs_f32() / target_duration.get() * 100.0; + let elapsed_time_bar = gauge(progress); + + let reset_button = button(|| "Reset").on_click_stop(move |_| elapsed_time.set(Duration::ZERO)); + + let view = v_stack(( + stack((text("Elapsed Time: "), elapsed_time_bar)).style(|s| s.justify_between()), + elapsed_time_label, + stack((text("Duration: "), duration_slider)).style(|s| s.justify_between()), + reset_button, + )) + .style(|s| s.gap(5, 5)); + + container(view).style(|s| { + s.size(100.pct(), 100.pct()) + .flex_col() + .items_center() + .justify_center() + }) +} + +/// A slider with a thin bar instead of the default thick bar. +fn thin_slider(fill_percent: impl Fn() -> f32 + 'static) -> slider::Slider { + slider::slider(fill_percent).style(|s| { + s.width(200) + .class(slider::AccentBarClass, |s| s.height(30.pct())) + .class(slider::BarClass, |s| s.height(30.pct())) + }) +} + +/// A non-interactive slider that has been repurposed into a progress bar. +fn gauge(fill_percent: impl Fn() -> f32 + 'static) -> slider::Slider { + slider::slider(fill_percent) + .disable_events(|| true) + .style(|s| { + s.width(200) + .set(slider::HandleRadius, 0.pct()) + .class(slider::BarClass, |s| s.set(BorderRadius, 25.pct())) + .class(slider::AccentBarClass, |s| s.set(BorderRadius, 25.pct())) + }) +}