Skip to content

Commit e55ee2d

Browse files
Address timing PR review feedback
1 parent 112f4b6 commit e55ee2d

12 files changed

Lines changed: 62 additions & 63 deletions

File tree

masonry/examples/layers.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ impl Widget for OverlayBox {
7777
) {
7878
if let PointerEvent::Move(PointerUpdate { current, .. }) = event {
7979
self.last_cursor_pos = current.logical_point();
80-
if let Some(timer) = self.hover_timer.take() {
81-
ctx.cancel_timer(timer);
80+
if self.layer_root_id.take().is_some() && ctx.is_hovered() && self.hover_timer.is_none()
81+
{
82+
self.hover_timer = Some(ctx.request_timer(Duration::from_millis(300)));
8283
}
83-
self.hover_timer = Some(ctx.request_timer(Duration::from_millis(300)));
8484
}
8585
}
8686

@@ -90,13 +90,24 @@ impl Widget for OverlayBox {
9090

9191
fn update(&mut self, ctx: &mut UpdateCtx<'_>, _props: &mut PropertiesMut<'_>, event: &Update) {
9292
match event {
93+
Update::HoveredChanged(true)
94+
if self.layer_root_id.is_none() && self.hover_timer.is_none() =>
95+
{
96+
self.hover_timer = Some(ctx.request_timer(Duration::from_millis(300)));
97+
}
9398
Update::HoveredChanged(false) => {
9499
if let Some(timer) = self.hover_timer.take() {
95100
ctx.cancel_timer(timer);
96101
}
102+
if let Some(layer) = self.layer_root_id.take() {
103+
ctx.remove_layer(layer);
104+
}
97105
}
98-
Update::Timer(token) if Some(*token) == self.hover_timer => {
106+
Update::TimerExpired(token) if Some(*token) == self.hover_timer => {
99107
self.hover_timer = None;
108+
if !ctx.is_hovered() {
109+
return;
110+
}
100111
let (overlay, layer_type) = (self.overlayer)();
101112
self.layer_root_id = Some(overlay.id());
102113
let layer_pos = self.last_cursor_pos + Vec2::new(5., -25.);

masonry/src/doc/implementing_widget.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ In the course of a frame, Masonry will run a series of passes over the widget tr
4848
- `on_pointer_event`, `on_text_event` and `on_access_event` are called once after a user-initiated event (like a mouse click or keyboard input).
4949
- `on_anim_frame` is called once per frame for animated widgets.
5050
- `update` is called many times during a frame, with various events reflecting changes in the widget's state (for instance, it gets or loses text focus).
51-
- `update` is also where timers requested by the widget are delivered as `Update::Timer`.
51+
- `update` is also where timers requested by the widget are delivered as `Update::TimerExpired`.
5252
- `measure` and `layout` are called during Masonry's layout pass.
5353
`measure` computes the preferred size of the widget on a single axis.
5454
`layout` receives a chosen size and lays out its children accordingly.
@@ -165,8 +165,8 @@ impl Widget for ColorRectangle {
165165

166166
Use `on_anim_frame` for visual state that should advance with frame cadence, such as continuous motion or interpolation.
167167
For delayed one-shot behavior, use a timer instead.
168-
For example, a widget can store an `Option<TimerToken>`, set it with `ctx.request_timer(delay)`, then compare it against `Update::Timer(token)` in `update`.
169-
If the delayed behavior should continue, request a new one-shot timer from that `Update::Timer` branch.
168+
For example, a widget can store an `Option<TimerToken>`, set it with `ctx.request_timer(delay)`, then compare it against `Update::TimerExpired(token)` in `update`.
169+
If the delayed behavior should continue, request a new one-shot timer from that `Update::TimerExpired` branch.
170170
Cancel the stored token with `ctx.cancel_timer(token)` when the state that made the timer relevant no longer applies.
171171

172172
### Layout

masonry/src/tests/update.rs

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use crate::core::{
1212
};
1313
use crate::layout::{AsUnit, Length};
1414
use crate::testing::{
15-
DebugName, ModularWidget, PRIMARY_MOUSE, Record, TestHarness, TestWidgetExt, assert_any,
16-
assert_debug_panics,
15+
DebugName, ModularWidget, PRIMARY_MOUSE, Record, TestHarness, TestWidgetExt, assert_all,
16+
assert_any, assert_debug_panics,
1717
};
1818
use crate::theme::test_property_set;
1919
use crate::util::Duration;
@@ -81,22 +81,18 @@ fn timer_update_targets_widget() {
8181
harness.flush_records_of(target_tag);
8282
harness.flush_records_of(parent_tag);
8383

84-
harness.set_timer_time(Duration::from_millis(10));
85-
assert_eq!(harness.handle_timers(), 1);
84+
harness.handle_timers(Duration::from_millis(10));
8685

8786
let records = harness.take_records_of(target_tag);
8887
assert_any(
8988
records,
90-
|record| matches!(record, Record::Update(Update::Timer(seen)) if seen == token),
89+
|record| matches!(record, Record::Update(Update::TimerExpired(seen)) if seen == token),
9190
);
9291

9392
let records = harness.take_records_of(parent_tag);
94-
assert!(
95-
!records
96-
.iter()
97-
.any(|record| matches!(record, Record::Update(Update::Timer(_)))),
98-
"timer update should not bubble to the parent"
99-
);
93+
assert_all(records, |record| {
94+
!matches!(record, Record::Update(Update::TimerExpired(_)))
95+
});
10096
}
10197

10298
#[test]
@@ -113,16 +109,12 @@ fn cancelled_timer_does_not_fire() {
113109
});
114110
harness.flush_records_of(target_tag);
115111

116-
harness.set_timer_time(Duration::from_millis(10));
117-
assert_eq!(harness.handle_timers(), 0);
112+
harness.handle_timers(Duration::from_millis(10));
118113

119114
let records = harness.take_records_of(target_tag);
120-
assert!(
121-
!records
122-
.iter()
123-
.any(|record| matches!(record, Record::Update(Update::Timer(_)))),
124-
"cancelled timer should not be delivered"
125-
);
115+
assert_all(records, |record| {
116+
!matches!(record, Record::Update(Update::TimerExpired(_)))
117+
});
126118
}
127119

128120
#[test]

masonry/src/widgets/text_area.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const CURSOR_BLINK_INTERVAL: u64 = 500;
3030
const CURSOR_BLINK_TIME: u64 = CURSOR_BLINK_INTERVAL * 2;
3131
/// The timeout after which the cursor will stop blinking and stay solid.
3232
const CURSOR_BLINK_TIMEOUT: u64 = 10_000;
33+
// TODO: These should be read from system settings, but we currently
34+
// aren't aware of a robust way to read that cross-platform.
3335

3436
/// `TextArea` implements the core of interactive text.
3537
///
@@ -284,8 +286,7 @@ impl<const EDITABLE: bool> TextArea<EDITABLE> {
284286
self.anim_prev_interval = self.anim_prev_interval.rem_euclid(CURSOR_BLINK_TIME);
285287
}
286288

287-
let should_show = self.anim_elapsed >= CURSOR_BLINK_TIMEOUT
288-
|| self.anim_prev_interval < CURSOR_BLINK_INTERVAL;
289+
let should_show = self.anim_elapsed >= CURSOR_BLINK_TIMEOUT || self.anim_prev_interval == 0;
289290
if self.anim_cursor_visible == should_show {
290291
false
291292
} else {
@@ -885,7 +886,7 @@ impl<const EDITABLE: bool> Widget for TextArea<EDITABLE> {
885886
// We might need to use the disabled brush, and stop displaying the selection.
886887
ctx.request_render();
887888
}
888-
Update::Timer(token) if Some(*token) == self.cursor_blink_timer => {
889+
Update::TimerExpired(token) if Some(*token) == self.cursor_blink_timer => {
889890
self.cursor_blink_timer = None;
890891
if ctx.is_window_focused() && ctx.is_focus_target() {
891892
if self.advance_cursor_blink() {

masonry/src/widgets/text_input.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,7 @@ mod tests {
414414
assert_render_snapshot!(harness, "text_input_selection_unfocused");
415415

416416
harness.process_text_event(TextEvent::WindowFocusChange(true));
417-
harness.set_timer_time(Duration::from_millis(500));
418-
harness.handle_timers();
417+
harness.handle_timers(Duration::from_millis(500));
419418

420419
assert_render_snapshot!(harness, "text_input_cursor_blink");
421420
}

masonry_core/src/app/render_root.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@ use crate::util::Duration;
4848
/// IME area as the `last_sent_ime_area`.
4949
const INVALID_IME_AREA: Rect = Rect::new(f64::NAN, f64::NAN, f64::NAN, f64::NAN);
5050

51-
fn duration_to_timer_ticks(duration: Duration) -> TimerInstant {
52-
u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
53-
}
54-
5551
// --- MARK: STRUCTS
5652

5753
/// The composition root of Masonry.
@@ -980,6 +976,10 @@ impl RenderRoot {
980976
/// The time is measured from an origin chosen by the host. All calls to
981977
/// this method and [`next_timer_deadline`](Self::next_timer_deadline) must
982978
/// use the same origin.
979+
///
980+
/// This is separate from [`handle_timers`](Self::handle_timers) because timer
981+
/// requests made while handling normal events also need to be scheduled
982+
/// relative to the current host time.
983983
pub fn set_timer_time(&mut self, time: Duration) {
984984
self.global_state.timer_now = duration_to_timer_ticks(time);
985985
}
@@ -994,9 +994,8 @@ impl RenderRoot {
994994

995995
/// Delivers all timers due at or before the current timer time.
996996
///
997-
/// Returns the number of timers delivered. Timers targeting removed widgets
998-
/// are discarded.
999-
pub fn handle_timers(&mut self) -> usize {
997+
/// Timers targeting removed widgets are discarded.
998+
pub fn handle_timers(&mut self) {
1000999
let now = self.global_state.timer_now;
10011000
let mut fired = Vec::new();
10021001
// Pop the due set before dispatch. Widget updates may request or cancel
@@ -1007,18 +1006,16 @@ impl RenderRoot {
10071006
fired.push((timer.into_target(), token));
10081007
}
10091008

1010-
let mut delivered = 0;
1009+
let had_due_timer = !fired.is_empty();
10111010
for (target, token) in fired {
10121011
if !self.widget_arena.has(target) {
10131012
continue;
10141013
}
10151014
run_update_timer_pass(self, target, token);
1016-
delivered += 1;
10171015
}
1018-
if delivered != 0 {
1016+
if had_due_timer {
10191017
self.run_rewrite_passes();
10201018
}
1021-
delivered
10221019
}
10231020

10241021
/// Returns true if the accessibility tree needs to be rebuilt.
@@ -1091,6 +1088,10 @@ impl RenderRootState {
10911088
}
10921089
}
10931090

1091+
fn duration_to_timer_ticks(duration: Duration) -> TimerInstant {
1092+
u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
1093+
}
1094+
10941095
impl RenderRootSignal {
10951096
pub(crate) fn new_ime_moved_signal(area: Rect) -> Self {
10961097
Self::ImeMoved(

masonry_core/src/core/contexts.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,11 +441,11 @@ impl_context_method!(
441441
{
442442
/// Requests a timer for the current widget.
443443
///
444-
/// When the timer expires, this widget receives [`Update::Timer`] with
444+
/// When the timer expires, this widget receives [`Update::TimerExpired`] with
445445
/// the returned token. Timer delivery is best-effort: timers targeting
446446
/// removed widgets are ignored by the host.
447447
///
448-
/// [`Update::Timer`]: crate::core::Update::Timer
448+
/// [`Update::TimerExpired`]: crate::core::Update::TimerExpired
449449
pub fn request_timer(&mut self, delay: Duration) -> TimerToken {
450450
self.global_state.request_timer(self.widget_id(), delay)
451451
}

masonry_core/src/core/events.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::util::Duration;
1414
///
1515
/// Timer tokens are assigned by a [`RenderRoot`](crate::app::RenderRoot)'s
1616
/// timer queue. A widget can keep the token to recognize a later
17-
/// [`Update::Timer`] or cancel the timer before it fires.
17+
/// [`Update::TimerExpired`] or cancel the timer before it fires.
1818
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1919
pub struct TimerToken(pub(crate) TimerId);
2020

@@ -137,7 +137,7 @@ pub enum Update {
137137
ChildHoveredChanged(bool),
138138

139139
/// Called when a timer requested by this widget expires.
140-
Timer(TimerToken),
140+
TimerExpired(TimerToken),
141141

142142
/// Called when the [active] status of the current widget changes.
143143
///
@@ -303,7 +303,7 @@ impl Update {
303303
Self::FocusChanged(true) => "FocusChanged(true)",
304304
Self::ChildFocusChanged(true) => "ChildFocusChanged(true)",
305305
Self::RequestPanToChild(_) => "RequestPanToChild(_)",
306-
Self::Timer(_) => "Timer(_)",
306+
Self::TimerExpired(_) => "TimerExpired(_)",
307307
Self::FontsChanged => "FontsChanged",
308308
}
309309
}

masonry_core/src/doc/pass_system.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ The animation pass may be considered as a special event pass: it's not triggered
5151
### Timer delivery
5252

5353
Widget timers are delayed callbacks requested with context methods such as [`UpdateCtx::request_timer`].
54-
When a timer expires, Masonry delivers [`Update::Timer`] to the widget that requested it.
54+
When a timer expires, Masonry delivers [`Update::TimerExpired`] to the widget that requested it.
5555

5656
Timer delivery does not bubble to ancestors.
5757
Like the animation pass, it is triggered externally by the host event loop and sets off the rewrite passes after delivery.
@@ -265,7 +265,7 @@ They can access the layout of children if they have already been laid out.
265265
[`RegisterCtx`]: crate::core::RegisterCtx
266266
[`QueryCtx`]: crate::core::QueryCtx
267267
[`WidgetAdded`]: crate::core::Update::WidgetAdded
268-
[`Update::Timer`]: crate::core::Update::Timer
268+
[`Update::TimerExpired`]: crate::core::Update::TimerExpired
269269
[`UpdateCtx::request_timer`]: crate::core::UpdateCtx::request_timer
270270
[`Ime::Disabled`]: crate::core::Ime::Disabled
271271
[`FocusChanged`]: crate::core::Update::FocusChanged

masonry_core/src/passes/update.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1248,11 +1248,14 @@ pub(crate) fn run_update_fonts_pass(root: &mut RenderRoot) {
12481248
);
12491249
}
12501250

1251+
// ----------------
1252+
1253+
// --- MARK: TIMERS
12511254
pub(crate) fn run_update_timer_pass(root: &mut RenderRoot, target: WidgetId, token: TimerToken) {
12521255
let _span = info_span!("update_timer").entered();
12531256

12541257
run_single_update_pass(root, Some(target), |widget, ctx, props| {
1255-
widget.update(ctx, props, &Update::Timer(token));
1258+
widget.update(ctx, props, &Update::TimerExpired(token));
12561259
});
12571260
}
12581261

0 commit comments

Comments
 (0)