Files
Ely-GPUI-Components/src/misc/stopwatch.rs
T
ZacharyZhang-NYandClaude Opus 5.5 977ba4c335 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>
2026-09-28 00:21:23 -04:00

250 lines
8.4 KiB
Rust

use std::time::{Duration, Instant};
use gpui::{
App, ElementId, FocusHandle, InteractiveElement, IntoElement, ParentElement, RenderOnce,
Styled, Task, Window, div,
};
use crate::{
buttons::{Button, ButtonVariant},
primitives::tab_stop,
theme::{ActiveTheme, TextSize},
typography::tabular,
};
const STEP: Duration = Duration::from_millis(100);
/// Time run while started and held while paused, the laps taken, and the task that wakes the face while it runs.
#[derive(Default)]
struct Run {
held: Duration,
since: Option<Instant>,
laps: Vec<Duration>,
tick: Option<Task<()>>,
}
impl Run {
fn elapsed(&self, now: Instant) -> Duration {
self.held
+ self
.since
.map_or(Duration::ZERO, |since| now.saturating_duration_since(since))
}
fn start(&mut self, now: Instant) {
self.since.get_or_insert(now);
}
fn pause(&mut self, now: Instant) {
if let Some(since) = self.since.take() {
self.held += now.saturating_duration_since(since);
}
self.tick = None;
}
/// Marks the time so far; only while running.
fn lap(&mut self, now: Instant) {
if self.since.is_some() {
self.laps.push(self.elapsed(now));
}
}
fn reset(&mut self) {
*self = Run::default();
}
}
/// Minutes, seconds and tenths, as 01:02.3; hours ahead of them once there are any.
pub(crate) fn tenths(elapsed: Duration) -> String {
let tenths = elapsed.as_millis() / 100;
let (hours, minutes, seconds, tenth) = (
tenths / 36_000,
tenths / 600 % 60,
tenths / 10 % 60,
tenths % 10,
);
match hours {
0 => format!("{minutes:02}:{seconds:02}.{tenth}"),
_ => format!("{hours}:{minutes:02}:{seconds:02}.{tenth}"),
}
}
/// 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,
}
impl Stopwatch {
pub fn new(id: impl Into<ElementId>) -> Self {
Self { id: id.into() }
}
}
impl RenderOnce for Stopwatch {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id.clone();
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();
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 {
cx.background_executor().timer(STEP).await;
let woke = cx.update(|_, cx| {
cx.notify(view);
weak.read_with(cx, |run, _| run.since.is_some())
});
if !matches!(woke, Ok(Ok(true))) {
break;
}
}
});
run.update(cx, |run, _| run.tick = Some(task));
}
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), 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()
.border_b_1()
.border_color(theme.colors.border)
.text_color(theme.colors.fg_muted)
.child(div().flex_1().child(format!("Lap {}", ix + 1)))
.child(
tabular(div())
.flex_none()
.text_color(theme.colors.fg)
.child(own.clone()),
)
.child(tabular(div()).flex_none().child(tenths(*total)))
});
div()
.flex()
.flex_col()
.gap_3()
.child(
tabular(div())
.debug_selector(|| format!("stopwatch-{shown}"))
.text_size(theme.text_size(TextSize::Xxl))
.text_color(theme.colors.fg)
.child(shown.clone()),
)
.child(
div()
.flex()
.flex_wrap()
.gap_2()
.child(
Button::new(
(id.clone(), "start"),
if running { "Pause" } else { "Start" },
)
.variant(if running {
ButtonVariant::Secondary
} else {
ButtonVariant::Primary
})
.focus_handle(&toggle)
.on_click(match running {
true => act("pause", Run::pause, None),
false => act("start", Run::start, None),
}),
)
.child(
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))
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use super::{Run, tenths};
#[test]
fn a_readout_shows_minutes_seconds_and_tenths() {
assert_eq!(tenths(Duration::ZERO), "00:00.0");
assert_eq!(tenths(Duration::from_millis(62_345)), "01:02.3");
assert_eq!(tenths(Duration::from_millis(3_723_450)), "1:02:03.4");
assert_eq!(tenths(Duration::from_millis(99)), "00:00.0", "tenths floor");
}
#[test]
fn a_run_holds_while_paused_and_laps_only_while_running() {
let start = Instant::now();
let at = |ms: u64| start + Duration::from_millis(ms);
let mut run = Run::default();
run.start(at(0));
run.lap(at(1_200));
run.pause(at(2_000));
assert_eq!(
run.elapsed(at(9_000)),
Duration::from_millis(2_000),
"paused, it holds"
);
run.lap(at(9_000));
assert_eq!(
run.laps,
[Duration::from_millis(1_200)],
"no lap while paused"
);
run.start(at(10_000));
run.start(at(10_500));
assert_eq!(
run.elapsed(at(11_000)),
Duration::from_millis(3_000),
"a second start keeps the first"
);
run.reset();
assert_eq!(
(run.elapsed(at(12_000)), run.laps.len()),
(Duration::ZERO, 0)
);
}
}