diff --git a/masonry/examples/animation.rs b/masonry/examples/animation.rs new file mode 100644 index 000000000..4042e440c --- /dev/null +++ b/masonry/examples/animation.rs @@ -0,0 +1,49 @@ +// Copyright 2019 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +//! This is a very small example of how to setup a masonry application. +//! It does the almost bare minimum while still being useful. + +// On Windows platform, don't show a console when opening the app. +//#![windows_subsystem = "windows"] + +use masonry::dpi::LogicalSize; +use masonry::text::StyleProperty; +use masonry::widget::{Button, Flex, Label, ProgressBar, RootWidget}; +use masonry::{Action, AppDriver, DriverCtx, FontWeight, WidgetId}; +use winit::window::Window; + +const VERTICAL_WIDGET_SPACING: f64 = 20.0; + +struct Driver; + +impl AppDriver for Driver { + fn on_action(&mut self, _ctx: &mut DriverCtx<'_>, _widget_id: WidgetId, action: Action) { + match action { + Action::ButtonPressed(_) => { + println!("Hello"); + } + action => { + eprintln!("Unexpected action {action:?}"); + } + } + } +} + +fn main() { + let progress_bar = ProgressBar::new(None).animate(true); + + let window_size = LogicalSize::new(400.0, 400.0); + let window_attributes = Window::default_attributes() + .with_title("Hello World!") + .with_resizable(true) + .with_min_inner_size(window_size); + + masonry::event_loop_runner::run( + masonry::event_loop_runner::EventLoop::with_user_event(), + window_attributes, + RootWidget::new(progress_bar), + Driver, + ) + .unwrap(); +} diff --git a/masonry/src/contexts.rs b/masonry/src/contexts.rs index da78ce259..2ca4c9bf8 100644 --- a/masonry/src/contexts.rs +++ b/masonry/src/contexts.rs @@ -3,7 +3,7 @@ //! The context types that are passed into various widget methods. -use std::time::Duration; +use std::time::{Duration, Instant}; use accesskit::TreeUpdate; use dpi::LogicalPosition; @@ -19,6 +19,8 @@ use crate::passes::layout::run_layout_on; use crate::render_root::{MutateCallback, RenderRootSignal, RenderRootState}; use crate::text::BrushIndex; use crate::theme::get_debug_color; +use crate::timers::Timer; +pub use crate::timers::TimerId; use crate::widget::{WidgetMut, WidgetRef, WidgetState}; use crate::{AllowRawMut, BoxConstraints, Insets, Point, Rect, Size, Widget, WidgetId, WidgetPod}; @@ -713,8 +715,13 @@ impl_context_method!( /// /// The return value is a token, which can be used to associate the /// request with the event. - pub fn request_timer(&mut self, _deadline: Duration) -> TimerToken { - todo!("request_timer"); + pub fn request_timer(&mut self, deadline: Duration) -> TimerId { + let deadline = Instant::now() + deadline; + let timer = Timer::new(self.widget_id(), deadline); + let id = timer.id; + self.global_state + .emit_signal(RenderRootSignal::TimerRequested(timer)); + id } /// Mark child widget as stashed. @@ -737,9 +744,6 @@ impl_context_method!( } ); -// FIXME - Remove -pub struct TimerToken; - impl EventCtx<'_> { // TODO - clearly document all semantics of pointer capture when they've been decided on // TODO - Figure out cases where widget should be notified of pointer capture diff --git a/masonry/src/event.rs b/masonry/src/event.rs index 29294f62c..cd40640b8 100644 --- a/masonry/src/event.rs +++ b/masonry/src/event.rs @@ -4,12 +4,14 @@ //! Events. use std::path::PathBuf; +use std::time::Instant; use winit::event::{Force, Ime, KeyEvent, Modifiers}; use winit::keyboard::ModifiersState; use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; use crate::kurbo::Rect; +use crate::timers::TimerId; // TODO - Occluded(bool) event // TODO - winit ActivationTokenDone thing @@ -213,6 +215,12 @@ pub struct AccessEvent { pub data: Option, } +#[derive(Debug, Clone, Copy)] +pub struct TimerEvent { + pub id: TimerId, + pub deadline: Instant, +} + #[derive(Debug, Clone)] pub struct PointerState { // TODO diff --git a/masonry/src/event_loop_runner.rs b/masonry/src/event_loop_runner.rs index dda6a5636..ec052fb8f 100644 --- a/masonry/src/event_loop_runner.rs +++ b/masonry/src/event_loop_runner.rs @@ -3,6 +3,7 @@ use std::num::NonZeroUsize; use std::sync::Arc; +use std::time::Instant; use accesskit_winit::Adapter; use tracing::{debug, info_span, warn}; @@ -17,13 +18,14 @@ use winit::event::{ DeviceEvent as WinitDeviceEvent, DeviceId, MouseButton as WinitMouseButton, WindowEvent as WinitWindowEvent, }; -use winit::event_loop::ActiveEventLoop; +use winit::event_loop::{ActiveEventLoop, ControlFlow}; use winit::window::{Window, WindowAttributes, WindowId}; use crate::app_driver::{AppDriver, DriverCtx}; use crate::dpi::LogicalPosition; use crate::event::{PointerButton, PointerState, WindowEvent}; use crate::render_root::{self, RenderRoot, WindowSizePolicy}; +use crate::timers::TimerQueue; use crate::{PointerEvent, TextEvent, Widget, WidgetId}; #[derive(Debug)] @@ -81,6 +83,7 @@ pub struct MasonryState<'a> { proxy: EventLoopProxy, #[cfg(feature = "tracy")] frame: Option, + timers: TimerQueue, // Per-Window state // In future, this will support multiple windows @@ -159,6 +162,14 @@ impl ApplicationHandler for MainState<'_> { self.masonry_state.handle_suspended(event_loop); } + fn new_events( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + cause: winit::event::StartCause, + ) { + self.masonry_state.handle_new_events(event_loop, cause); + } + fn window_event( &mut self, event_loop: &ActiveEventLoop, @@ -200,14 +211,6 @@ impl ApplicationHandler for MainState<'_> { self.masonry_state.handle_about_to_wait(event_loop); } - fn new_events( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - cause: winit::event::StartCause, - ) { - self.masonry_state.handle_new_events(event_loop, cause); - } - fn exiting(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { self.masonry_state.handle_exiting(event_loop); } @@ -244,6 +247,7 @@ impl MasonryState<'_> { frame: None, pointer_state: PointerState::empty(), proxy: event_loop.create_proxy(), + timers: TimerQueue::new(), window: WindowState::Uninitialized(window), background_color, @@ -658,10 +662,35 @@ impl MasonryState<'_> { self.handle_signals(event_loop, app_driver); } - // --- MARK: EMPTY WINIT HANDLERS --- - pub fn handle_about_to_wait(&mut self, _: &ActiveEventLoop) {} + // --- MARK: TIMERS --- + pub fn handle_new_events(&mut self, _: &ActiveEventLoop, _: winit::event::StartCause) { + // check if timers have elapsed and set event loop to wake at next timer deadline + let now = Instant::now(); + loop { + let Some(next_timer) = self.timers.peek() else { + break; + }; + if next_timer.deadline > now { + break; + } + // timer has elapsed - remove from heap and handle + let Some(elapsed_timer) = self.timers.pop() else { + debug_panic!("should be unreachable: peek was Some"); + break; + }; + self.render_root.handle_elapsed_timer(elapsed_timer); + } + } - pub fn handle_new_events(&mut self, _: &ActiveEventLoop, _: winit::event::StartCause) {} + pub fn handle_about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if let Some(next_timer) = self.timers.peek() { + event_loop.set_control_flow(ControlFlow::WaitUntil(next_timer.deadline)); + } else { + event_loop.set_control_flow(ControlFlow::Wait); + } + } + + // --- MARK: EMPTY WINIT HANDLERS --- pub fn handle_exiting(&mut self, _: &ActiveEventLoop) {} @@ -686,6 +715,9 @@ impl MasonryState<'_> { app_driver.on_action(&mut driver_ctx, widget_id, action); }); } + render_root::RenderRootSignal::TimerRequested(timer) => { + self.timers.push(timer); + } render_root::RenderRootSignal::StartIme => { window.set_ime_allowed(true); } diff --git a/masonry/src/lib.rs b/masonry/src/lib.rs index a948bb28d..601c7356e 100644 --- a/masonry/src/lib.rs +++ b/masonry/src/lib.rs @@ -161,6 +161,7 @@ mod event; mod paint_scene_helpers; mod passes; mod render_root; +mod timers; mod tracing_backend; pub mod event_loop_runner; diff --git a/masonry/src/passes/event.rs b/masonry/src/passes/event.rs index 3a9e3c23a..95902f9a7 100644 --- a/masonry/src/passes/event.rs +++ b/masonry/src/passes/event.rs @@ -6,8 +6,10 @@ use tracing::{debug, info_span, trace}; use winit::event::ElementState; use winit::keyboard::{KeyCode, PhysicalKey}; +use crate::event::TimerEvent; use crate::passes::{enter_span, merge_state_up}; use crate::render_root::RenderRoot; +use crate::timers::Timer; use crate::{AccessEvent, EventCtx, Handled, PointerEvent, TextEvent, Widget, WidgetId}; // --- MARK: HELPERS --- @@ -135,6 +137,26 @@ pub(crate) fn run_on_pointer_event_pass(root: &mut RenderRoot, event: &PointerEv handled } +pub(crate) fn run_on_elapsed_timer_pass(root: &mut RenderRoot, timer: &Timer) -> Handled { + let event = TimerEvent { + deadline: timer.deadline, + id: timer.id, + }; + let handled = run_event_pass( + root, + Some(timer.widget_id), + &event, + false, + |widget, ctx, event| { + widget.on_timer_expired(ctx, event); + // don't traverse for this event + ctx.set_handled(); + }, + true, + ); + handled +} + // TODO https://github.com/linebender/xilem/issues/376 - Some implicit invariants: // - If a Widget gets a keyboard event or an ImeStateChange, then // focus is on it, its child or its parent. diff --git a/masonry/src/render_root.rs b/masonry/src/render_root.rs index d52df7c76..4df8f6d85 100644 --- a/masonry/src/render_root.rs +++ b/masonry/src/render_root.rs @@ -24,7 +24,8 @@ use crate::passes::accessibility::run_accessibility_pass; use crate::passes::anim::run_update_anim_pass; use crate::passes::compose::run_compose_pass; use crate::passes::event::{ - run_on_access_event_pass, run_on_pointer_event_pass, run_on_text_event_pass, + run_on_access_event_pass, run_on_elapsed_timer_pass, run_on_pointer_event_pass, + run_on_text_event_pass, }; use crate::passes::layout::run_layout_pass; use crate::passes::mutate::{mutate_widget, run_mutate_pass}; @@ -36,6 +37,7 @@ use crate::passes::update::{ }; use crate::passes::{recurse_on_children, PassTracing}; use crate::text::BrushIndex; +use crate::timers::Timer; use crate::widget::{WidgetArena, WidgetMut, WidgetRef, WidgetState}; use crate::{AccessEvent, Action, CursorIcon, Handled, QueryCtx, Widget, WidgetId, WidgetPod}; @@ -123,6 +125,7 @@ pub struct RenderRootOptions { pub enum RenderRootSignal { Action(Action, WidgetId), + TimerRequested(Timer), StartIme, EndIme, ImeMoved(LogicalPosition, LogicalSize), @@ -259,6 +262,14 @@ impl RenderRoot { } // --- MARK: PUB FUNCTIONS --- + pub fn handle_elapsed_timer(&mut self, timer: Timer) -> Handled { + let _span = info_span!("elapsed_timer"); + let handled = run_on_elapsed_timer_pass(self, &timer); + self.run_rewrite_passes(); + + handled + } + pub fn handle_pointer_event(&mut self, event: PointerEvent) -> Handled { let _span = info_span!("pointer_event"); let handled = run_on_pointer_event_pass(self, &event); diff --git a/masonry/src/testing/harness.rs b/masonry/src/testing/harness.rs index 938df768e..44c6622fe 100644 --- a/masonry/src/testing/harness.rs +++ b/masonry/src/testing/harness.rs @@ -25,6 +25,7 @@ use crate::passes::anim::run_update_anim_pass; use crate::render_root::{RenderRoot, RenderRootOptions, RenderRootSignal, WindowSizePolicy}; use crate::testing::screenshots::get_image_diff; use crate::testing::snapshot_utils::get_cargo_workspace; +use crate::timers::TimerQueue; use crate::tracing_backend::try_init_test_tracing; use crate::widget::{WidgetMut, WidgetRef}; use crate::{Color, Handled, Point, Size, Vec2, Widget, WidgetId}; @@ -116,6 +117,7 @@ pub struct TestHarness { has_ime_session: bool, ime_rect: (LogicalPosition, LogicalSize), title: String, + timers: TimerQueue, } /// Assert a snapshot of a rendered frame of your app. @@ -199,6 +201,7 @@ impl TestHarness { has_ime_session: false, ime_rect: Default::default(), title: String::new(), + timers: TimerQueue::new(), }; harness.process_window_event(WindowEvent::Resize(window_size)); @@ -244,6 +247,9 @@ impl TestHarness { RenderRootSignal::Action(action, widget_id) => { self.action_queue.push_back((action, widget_id)); } + RenderRootSignal::TimerRequested(timer) => { + self.timers.push(timer); + } RenderRootSignal::StartIme => { self.has_ime_session = true; } diff --git a/masonry/src/timers.rs b/masonry/src/timers.rs new file mode 100644 index 000000000..e75e38c4c --- /dev/null +++ b/masonry/src/timers.rs @@ -0,0 +1,85 @@ +use std::{ + collections::BinaryHeap, + num::NonZeroU64, + sync::atomic::{AtomicU64, Ordering}, + time::Instant, +}; + +use crate::WidgetId; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +pub struct TimerId(NonZeroU64); + +impl TimerId { + pub fn next() -> TimerId { + static TIMER_ID_COUNTER: AtomicU64 = AtomicU64::new(1); + let id = TIMER_ID_COUNTER.fetch_add(1, Ordering::Relaxed); + TimerId(id.try_into().unwrap()) + } +} +/// An ordered list of timers set by masonry +/// +/// Implemented as a min priority queue +pub struct TimerQueue { + queue: BinaryHeap, +} + +impl TimerQueue { + pub fn new() -> Self { + Self { + queue: BinaryHeap::new(), + } + } + + pub fn push(&mut self, timer: Timer) { + self.queue.push(timer); + } + + /// Copy and return the `Instant` at the head of the queue + pub fn peek(&self) -> Option { + self.queue.peek().map(|v| *v) + } + + /// Remove the `Instant` at the head of the queue and return it + pub fn pop(&mut self) -> Option { + self.queue.pop() + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct Timer { + pub id: TimerId, + pub widget_id: WidgetId, + pub deadline: Instant, +} + +impl Timer { + pub fn new(widget_id: WidgetId, deadline: Instant) -> Self { + Self { + id: TimerId::next(), + widget_id, + deadline, + } + } +} + +// We implement `Ord` first by comparing `deadline`, and then +// `id`. This way, we ensure that timers with the same expiry +// time will trigger in the order they were created. +// +// Because Rust std's `BinaryHeap` is max-first, we need to reverse +// both comparisons. +impl Ord for Timer { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.deadline + .cmp(&other.deadline) + .reverse() + .then(self.id.cmp(&other.id).reverse()) + } +} + +impl PartialOrd for Timer { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/masonry/src/widget/progress_bar.rs b/masonry/src/widget/progress_bar.rs index 64b53b5b7..0d6001ef4 100644 --- a/masonry/src/widget/progress_bar.rs +++ b/masonry/src/widget/progress_bar.rs @@ -3,11 +3,15 @@ //! A progress bar widget. +use std::time::Duration; + use accesskit::{Node, Role}; use smallvec::{smallvec, SmallVec}; use tracing::{trace_span, Span}; use vello::Scene; +use crate::contexts::TimerId; +use crate::event::TimerEvent; use crate::kurbo::Size; use crate::paint_scene_helpers::{fill_lin_gradient, stroke, UnitPoint}; use crate::text::ArcStr; @@ -27,6 +31,10 @@ pub struct ProgressBar { /// It is also used if an invalid float (outside of [0, 1]) is passed. progress: Option, label: WidgetPod