Misc · Time: Lap turns Reset, laps and tickers tested

The stopwatch's second button reads Lap while running and Reset once
paused, on one focus handle, and Reset hands focus to Start, so no
press leaves focus stranded. Lap rows carry selectors; tests hold
their order and times, count the tickers' wakes, and a WorldClock
without places fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-28 00:21:23 -04:00
1 parent 6918370b08
commit 977ba4c335
4 files changed
+127 -42

No files matched your search

+1 -1
View File
@@ -64,7 +64,7 @@ fn clocks(cx: &App) -> impl IntoElement + use<> {
fn stopwatch(cx: &App) -> impl IntoElement + use<> {
section(
"Stopwatch",
"Time that runs while started and holds while paused, in tenths; Lap marks the time so far, Reset clears it once paused.",
"Time that runs while started and holds while paused, in tenths. Lap marks the time so far; once paused, it reads Reset and clears it.",
cx,
)
.child(probe("stopwatch-start", div().w(px(320.0)).child(Stopwatch::new("stopwatch"))))
+15 -6
View File
@@ -187,7 +187,7 @@ impl RenderOnce for Clock {
}
}
/// Places and their zones, a row each: a small face, the place, how its day and hour stand against home, and its time. Home is the system zone unless told; it ticks each second, or shows the owner's moment.
/// Places and their zones, a row each: a small face, the place, how its day and hour stand against home, and its time; fails without places. Home is the system zone unless told; it ticks each second, or shows the owner's moment.
#[derive(IntoElement)]
pub struct WorldClock {
id: ElementId,
@@ -201,12 +201,15 @@ impl WorldClock {
id: impl Into<ElementId>,
places: impl IntoIterator<Item = (impl Into<SharedString>, TimeZone)>,
) -> Self {
let id = id.into();
let places: Vec<_> = places
.into_iter()
.map(|(name, zone)| (name.into(), zone))
.collect();
assert!(!places.is_empty(), "world clock {id:?} has no places");
Self {
id: id.into(),
places: places
.into_iter()
.map(|(name, zone)| (name.into(), zone))
.collect(),
id,
places,
home: None,
now: None,
}
@@ -291,6 +294,12 @@ mod tests {
assert_eq!(second, 0.25);
}
#[test]
#[should_panic(expected = "has no places")]
fn a_world_clock_without_places_fails() {
super::WorldClock::new("world", Vec::<(&str, TimeZone)>::new());
}
#[test]
fn a_zone_stands_against_home_by_day_and_offset() {
let now: Timestamp = "2026-09-28T03:00:00Z".parse().expect("a moment");
+37 -22
View File
@@ -1,8 +1,8 @@
use std::time::{Duration, Instant};
use gpui::{
App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, Styled, Task,
Window, div,
App, ElementId, FocusHandle, InteractiveElement, IntoElement, ParentElement, RenderOnce,
Styled, Task, Window, div,
};
use crate::{
@@ -69,7 +69,7 @@ pub(crate) fn tenths(elapsed: Duration) -> String {
}
}
/// Time that runs while started and holds while paused, in tenths, with Start or Pause, Lap and Reset under it and the laps below, newest first: each lap's own time and the total at its end. It reads the executor's clock.
/// Time that runs while started and holds while paused, in tenths, with Start or Pause under it beside Lap, which reads Reset once paused, and the laps below, newest first: each lap's own time and the total at its end. It reads the executor's clock.
#[derive(IntoElement)]
pub struct Stopwatch {
id: ElementId,
@@ -87,7 +87,14 @@ impl RenderOnce for Stopwatch {
let run = window.use_keyed_state((id.clone(), "run"), cx, |_, _| Run::default());
let toggle = tab_stop((id.clone(), "toggle").into(), true, window, cx);
let now = cx.background_executor().now();
if run.read(cx).since.is_some() && run.read(cx).tick.is_none() {
let (running, elapsed) = (run.read(cx).since.is_some(), run.read(cx).elapsed(now));
let side = tab_stop(
(id.clone(), "side").into(),
running || !elapsed.is_zero(),
window,
cx,
);
if running && run.read(cx).tick.is_none() {
let (view, weak) = (window.current_view(), run.downgrade());
let task = window.spawn(cx, async move |cx| {
loop {
@@ -103,26 +110,28 @@ impl RenderOnce for Stopwatch {
});
run.update(cx, |run, _| run.tick = Some(task));
}
let (running, elapsed, laps) = {
let run = run.read(cx);
(run.since.is_some(), run.elapsed(now), run.laps.clone())
};
let laps = run.read(cx).laps.clone();
let theme = cx.theme();
let shown = tenths(elapsed);
let act = |name: &'static str, edit: fn(&mut Run, Instant)| {
let run = run.clone();
move |_: &gpui::ClickEvent, _: &mut Window, cx: &mut App| {
let act = |name: &'static str, edit: fn(&mut Run, Instant), then: Option<&FocusHandle>| {
let (run, then) = (run.clone(), then.cloned());
move |_: &gpui::ClickEvent, window: &mut Window, cx: &mut App| {
let now = cx.background_executor().now();
run.update(cx, |run, cx| {
edit(run, now);
log::info!("stopwatch: {name} at {}", tenths(run.elapsed(now)));
cx.notify();
});
if let Some(then) = &then {
window.focus(then);
}
}
};
let rows = laps.iter().enumerate().rev().map(|(ix, total)| {
let before = ix.checked_sub(1).map_or(Duration::ZERO, |last| laps[last]);
let own = tenths(*total - before);
div()
.debug_selector(|| format!("lap-{}-{own}-{}", ix + 1, tenths(*total)))
.flex()
.gap_3()
.py_1()
@@ -134,7 +143,7 @@ impl RenderOnce for Stopwatch {
tabular(div())
.flex_none()
.text_color(theme.colors.fg)
.child(tenths(*total - before)),
.child(own.clone()),
)
.child(tabular(div()).flex_none().child(tenths(*total)))
});
@@ -166,19 +175,25 @@ impl RenderOnce for Stopwatch {
})
.focus_handle(&toggle)
.on_click(match running {
true => act("pause", Run::pause),
false => act("start", Run::start),
true => act("pause", Run::pause, None),
false => act("start", Run::start, None),
}),
)
.child(
Button::new((id.clone(), "lap"), "Lap")
.disabled(!running)
.on_click(act("lap", Run::lap)),
)
.child(
Button::new((id.clone(), "reset"), "Reset")
.disabled(running || elapsed.is_zero())
.on_click(act("reset", |run, _| run.reset())),
Button::new(
(id.clone(), "side"),
if running || elapsed.is_zero() {
"Lap"
} else {
"Reset"
},
)
.focus_handle(&side)
.disabled(!running && elapsed.is_zero())
.on_click(match running {
true => act("lap", Run::lap, None),
false => act("reset", |run, _| run.reset(), Some(&toggle)),
}),
),
)
.child(div().flex().flex_col().children(rows))
+74 -13
View File
@@ -1,4 +1,4 @@
use std::time::Duration;
use std::{cell::Cell, rc::Rc, time::Duration};
use gpui::{
AnyElement, Context, FocusHandle, IntoElement, KeyUpEvent, Keystroke, ParentElement, Render,
@@ -34,7 +34,11 @@ fn settle(cx: &mut VisualTestContext) {
cx.run_until_parked();
}
fn stage(part: fn() -> AnyElement, cx: &mut TestAppContext) -> &mut VisualTestContext {
/// Stages `part`, counting the times the stage is told to redraw.
fn stage(
part: fn() -> AnyElement,
cx: &mut TestAppContext,
) -> (Rc<Cell<usize>>, &mut VisualTestContext) {
cx.update(|cx| {
Theme::init(cx);
cx.bind_keys([
@@ -47,9 +51,15 @@ fn stage(part: fn() -> AnyElement, cx: &mut TestAppContext) -> &mut VisualTestCo
part,
});
let root = view.read_with(cx, |stage, _| stage.root.clone());
cx.update(|window, _| window.focus(&root));
let wakes = Rc::new(Cell::new(0));
let count = wakes.clone();
cx.update(|window, cx| {
window.focus(&root);
cx.observe(&view, move |_, _| count.set(count.get() + 1))
.detach();
});
settle(cx);
cx
(wakes, cx)
}
fn press(key: &str, cx: &mut VisualTestContext) {
@@ -74,31 +84,80 @@ fn watch() -> AnyElement {
Stopwatch::new("watch").into_any_element()
}
/// Start runs the time on the executor's clock, Pause holds it, and Reset clears it for the next run.
/// Start runs the time on the executor's clock, Lap lists the laps newest first, Pause holds it, and Reset clears it and hands focus to Start.
#[gpui::test]
fn a_stopwatch_runs_holds_and_resets(cx: &mut TestAppContext) {
let cx = stage(watch, cx);
fn a_stopwatch_runs_laps_holds_and_resets(cx: &mut TestAppContext) {
let (_, cx) = stage(watch, cx);
press("tab", cx);
press("enter", cx);
wait(1_250, cx);
assert!(shown("stopwatch-00:01.2", cx), "running");
wait(1_100, cx);
assert!(shown("stopwatch-00:01.1", cx), "running");
press("tab", cx);
press("enter", cx);
wait(1_200, cx);
press("enter", cx);
let row = |selector: &'static str, cx: &mut VisualTestContext| {
cx.debug_bounds(selector)
.unwrap_or_else(|| panic!("no row {selector}"))
};
assert!(
row("lap-2-00:01.2-00:02.3", cx).origin.y < row("lap-1-00:01.1-00:01.1", cx).origin.y,
"each lap's own time and total, the newest on top"
);
press("shift-tab", cx);
press("enter", cx);
wait(2_000, cx);
assert!(
shown("stopwatch-00:01.2", cx) && !shown("stopwatch-00:03.2", cx),
shown("stopwatch-00:02.3", cx) && !shown("stopwatch-00:04.3", cx),
"Pause, on the same button, holds it"
);
press("tab", cx);
press("enter", cx);
press("shift-tab", cx);
press("enter", cx);
wait(300, cx);
assert!(
shown("stopwatch-00:00.3", cx),
"Reset cleared it, and Start ran it from nothing"
"Lap, now Reset, cleared it and handed focus to Start"
);
}
/// The ticker wakes the stopwatch each tenth while it runs, and not at all once paused.
#[gpui::test]
fn a_running_stopwatch_wakes_each_tenth_and_rests_paused(cx: &mut TestAppContext) {
let (wakes, cx) = stage(watch, cx);
press("tab", cx);
press("enter", cx);
let started = wakes.get();
for _ in 0..12 {
wait(100, cx);
}
assert!(
wakes.get() - started >= 12,
"woke {} times",
wakes.get() - started
);
press("enter", cx);
let paused = wakes.get();
for _ in 0..10 {
wait(100, cx);
}
assert_eq!(wakes.get(), paused, "paused, it sleeps");
}
/// A clock on the wall clock wakes its view within each second.
#[gpui::test]
fn a_clock_ticks_each_second(cx: &mut TestAppContext) {
let (wakes, cx) = stage(
|| Clock::new("clock").zone(TimeZone::UTC).into_any_element(),
cx,
);
for second in 1..=2 {
let before = wakes.get();
wait(1_000, cx);
assert!(wakes.get() > before, "no tick in second {second}");
}
}
fn clocks() -> AnyElement {
let now: Timestamp = "2026-09-28T03:00:00Z".parse().expect("a moment");
let zone = |name: &str| TimeZone::get(name).expect("a zone");
@@ -126,6 +185,8 @@ fn clocks() -> AnyElement {
/// Clocks shown at the owner's moment draw without a ticker.
#[gpui::test]
fn clocks_draw_at_the_owners_moment(cx: &mut TestAppContext) {
let cx = stage(clocks, cx);
let (wakes, cx) = stage(clocks, cx);
let before = wakes.get();
wait(5_000, cx);
assert_eq!(wakes.get(), before, "nothing woke them");
}