Misc · Time: clocks and a stopwatch
misc::Clock shows the time in a zone, the system's unless told, as an analog face with hour marks and three hands over its digits and a label; it wakes its view at each whole second of the wall clock, and only while the owner passes no moment. misc::WorldClock lays places in rows: a small face, the place, how its day and hour stand against home, and its time. misc::Stopwatch runs on the executor's clock in tenths, holds while paused, marks laps while running and clears once paused; Start and Pause share one button and its focus.
This commit is contained in:
1 parent
4777cd666b
commit
45ae161c18
11 files changed
+790
-2
No files matched your search
@@ -0,0 +1,318 @@
|
||||
use std::{cmp::Ordering, f32::consts::TAU, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, IntoElement, ParentElement, Pixels, RenderOnce, SharedString, Styled, Task,
|
||||
Window, canvas, div, point,
|
||||
};
|
||||
use jiff::{Timestamp, civil::Time, tz::TimeZone};
|
||||
|
||||
use crate::{
|
||||
canvas::{finish, outline},
|
||||
theme::{ActiveTheme, TextSize},
|
||||
typography::{Ellipsis, format, tabular},
|
||||
};
|
||||
|
||||
/// Where each hand points, in turns clockwise from twelve: the hour's, the minute's and the second's.
|
||||
pub(crate) fn hands(time: Time) -> (f32, f32, f32) {
|
||||
let (hour, minute, second) = (
|
||||
f32::from(time.hour() % 12),
|
||||
f32::from(time.minute()),
|
||||
f32::from(time.second()),
|
||||
);
|
||||
(
|
||||
(hour + minute / 60.0 + second / 3600.0) / 12.0,
|
||||
(minute + second / 60.0) / 60.0,
|
||||
second / 60.0,
|
||||
)
|
||||
}
|
||||
|
||||
/// How a zone's day and hour stand against home's at `now`: today, tomorrow or yesterday, and its offset from home.
|
||||
pub(crate) fn against(now: Timestamp, zone: &TimeZone, home: &TimeZone) -> (&'static str, String) {
|
||||
let (there, here) = (now.to_zoned(zone.clone()), now.to_zoned(home.clone()));
|
||||
let day = match there.date().cmp(&here.date()) {
|
||||
Ordering::Greater => "Tomorrow",
|
||||
Ordering::Less => "Yesterday",
|
||||
Ordering::Equal => "Today",
|
||||
};
|
||||
let apart = there.offset().seconds() - here.offset().seconds();
|
||||
let (hours, minutes) = (apart.abs() / 3600, apart.abs() / 60 % 60);
|
||||
let sign = if apart < 0 { format::MINUS } else { '+' };
|
||||
let offset = match (hours, minutes) {
|
||||
(0, 0) => "Same time".to_string(),
|
||||
(_, 0) => format!("{sign}{hours}h"),
|
||||
_ => format!("{sign}{hours}h {minutes}m"),
|
||||
};
|
||||
(day, offset)
|
||||
}
|
||||
|
||||
/// Wakes the view at each whole second of the wall clock while its element renders.
|
||||
fn each_second(id: impl Into<ElementId>, window: &mut Window, cx: &mut App) {
|
||||
window.use_keyed_state(id, cx, |window, cx| -> Task<()> {
|
||||
cx.spawn_in(window, async move |ticker, cx| {
|
||||
loop {
|
||||
let past = Timestamp::now().subsec_nanosecond().max(0) as u64;
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_nanos(1_000_000_000 - past))
|
||||
.await;
|
||||
if ticker.update(cx, |_, cx| cx.notify()).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/// An analog face `side` wide: twelve hour marks, and hour, minute and second hands.
|
||||
fn face(time: Time, side: Pixels, cx: &App) -> impl IntoElement + use<> {
|
||||
let colors = cx.theme().colors.clone();
|
||||
let (hour, minute, second) = hands(time);
|
||||
let marks = colors.fg_subtle;
|
||||
div()
|
||||
.flex_none()
|
||||
.size(side)
|
||||
.rounded_full()
|
||||
.bg(colors.surface)
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.child(
|
||||
canvas(
|
||||
|_, _, _| {},
|
||||
move |bounds, _, window, _| {
|
||||
let r = f32::from(bounds.size.width) / 2.0;
|
||||
let at = |turn: f32, reach: f32| {
|
||||
let angle = turn * TAU;
|
||||
(r + angle.sin() * r * reach, r - angle.cos() * r * reach)
|
||||
};
|
||||
let width = |share: f32| Pixels::from(r * 2.0 * share);
|
||||
for mark in 0..12 {
|
||||
let turn = mark as f32 / 12.0;
|
||||
let line = [at(turn, 0.78), at(turn, 0.9)];
|
||||
finish(
|
||||
outline(&line, bounds.origin, width(0.02), false),
|
||||
marks,
|
||||
window,
|
||||
);
|
||||
}
|
||||
for (turn, reach, share, ink) in [
|
||||
(hour, 0.5, 0.045, colors.fg),
|
||||
(minute, 0.74, 0.03, colors.fg),
|
||||
(second, 0.84, 0.012, colors.accent),
|
||||
] {
|
||||
let line = [(r, r), at(turn, reach)];
|
||||
finish(
|
||||
outline(&line, bounds.origin, width(share), false),
|
||||
ink,
|
||||
window,
|
||||
);
|
||||
}
|
||||
let dot = width(0.06);
|
||||
let middle = bounds.origin
|
||||
+ point(Pixels::from(r) - dot / 2.0, Pixels::from(r) - dot / 2.0);
|
||||
window.paint_quad(
|
||||
gpui::fill(
|
||||
gpui::Bounds::new(middle, gpui::size(dot, dot)),
|
||||
colors.accent,
|
||||
)
|
||||
.corner_radii(dot / 2.0),
|
||||
);
|
||||
},
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The time in a zone, the system's unless told: an analog face over its digits, with a label under them. It ticks each second, or shows the owner's moment.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Clock {
|
||||
id: ElementId,
|
||||
zone: Option<TimeZone>,
|
||||
now: Option<Timestamp>,
|
||||
label: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Clock {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
zone: None,
|
||||
now: None,
|
||||
label: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zone(mut self, zone: TimeZone) -> Self {
|
||||
self.zone = Some(zone);
|
||||
self
|
||||
}
|
||||
|
||||
/// The moment it shows; the wall clock's otherwise.
|
||||
pub fn now(mut self, now: Timestamp) -> Self {
|
||||
self.now = Some(now);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Clock {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let zone = self.zone.unwrap_or_else(|| format::system_zone("clock"));
|
||||
if self.now.is_none() {
|
||||
each_second((self.id.clone(), "tick"), window, cx);
|
||||
}
|
||||
let now = self.now.unwrap_or_else(Timestamp::now).to_zoned(zone);
|
||||
let theme = cx.theme();
|
||||
let side = theme.misc().face.to_pixels(window.rem_size());
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(face(now.time(), side, cx))
|
||||
.child(
|
||||
tabular(div())
|
||||
.text_size(theme.text_size(TextSize::Lg))
|
||||
.text_color(theme.colors.fg)
|
||||
.child(now.strftime("%H:%M:%S").to_string()),
|
||||
)
|
||||
.children(self.label.map(|label| {
|
||||
div()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child(label)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[derive(IntoElement)]
|
||||
pub struct WorldClock {
|
||||
id: ElementId,
|
||||
places: Vec<(SharedString, TimeZone)>,
|
||||
home: Option<TimeZone>,
|
||||
now: Option<Timestamp>,
|
||||
}
|
||||
|
||||
impl WorldClock {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
places: impl IntoIterator<Item = (impl Into<SharedString>, TimeZone)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
places: places
|
||||
.into_iter()
|
||||
.map(|(name, zone)| (name.into(), zone))
|
||||
.collect(),
|
||||
home: None,
|
||||
now: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn home(mut self, home: TimeZone) -> Self {
|
||||
self.home = Some(home);
|
||||
self
|
||||
}
|
||||
|
||||
/// The moment it shows; the wall clock's otherwise.
|
||||
pub fn now(mut self, now: Timestamp) -> Self {
|
||||
self.now = Some(now);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for WorldClock {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let home = self
|
||||
.home
|
||||
.unwrap_or_else(|| format::system_zone("world clock"));
|
||||
if self.now.is_none() {
|
||||
each_second((self.id.clone(), "tick"), window, cx);
|
||||
}
|
||||
let now = self.now.unwrap_or_else(Timestamp::now);
|
||||
let theme = cx.theme();
|
||||
let side = theme.misc().small_face.to_pixels(window.rem_size());
|
||||
let rows = self.places.into_iter().map(|(name, zone)| {
|
||||
let (day, offset) = against(now, &zone, &home);
|
||||
let there = now.to_zoned(zone);
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.py_2()
|
||||
.border_b_1()
|
||||
.border_color(theme.colors.border)
|
||||
.child(face(there.time(), side, cx))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(div().text_color(theme.colors.fg).child(Ellipsis::new(name)))
|
||||
.child(
|
||||
div()
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child(Ellipsis::new(format!("{day}, {offset}"))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
tabular(div())
|
||||
.flex_none()
|
||||
.text_size(theme.text_size(TextSize::Lg))
|
||||
.text_color(theme.colors.fg)
|
||||
.child(there.strftime("%H:%M").to_string()),
|
||||
)
|
||||
});
|
||||
div().flex().flex_col().children(rows)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use jiff::{Timestamp, civil::time, tz::TimeZone};
|
||||
|
||||
use super::{against, hands};
|
||||
|
||||
#[test]
|
||||
fn hands_turn_with_the_hour_the_minute_and_the_second() {
|
||||
assert_eq!(hands(time(0, 0, 0, 0)), (0.0, 0.0, 0.0));
|
||||
assert_eq!(hands(time(15, 0, 0, 0)), (0.25, 0.0, 0.0), "three o'clock");
|
||||
let (hour, minute, second) = hands(time(9, 30, 15, 0));
|
||||
assert!(
|
||||
(hour - (9.5 + 15.0 / 3600.0) / 12.0).abs() < 1e-6,
|
||||
"the hour hand runs halfway on"
|
||||
);
|
||||
assert!((minute - 30.25 / 60.0).abs() < 1e-6);
|
||||
assert_eq!(second, 0.25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zone_stands_against_home_by_day_and_offset() {
|
||||
let now: Timestamp = "2026-09-28T03:00:00Z".parse().expect("a moment");
|
||||
let zone = |name: &str| TimeZone::get(name).expect("a zone in the database");
|
||||
let home = zone("America/New_York");
|
||||
assert_eq!(
|
||||
against(now, &zone("Asia/Tokyo"), &home),
|
||||
("Tomorrow", "+13h".into())
|
||||
);
|
||||
assert_eq!(
|
||||
against(now, &zone("Asia/Kolkata"), &home),
|
||||
("Tomorrow", "+9h 30m".into())
|
||||
);
|
||||
assert_eq!(
|
||||
against(now, &zone("America/Los_Angeles"), &home),
|
||||
("Today", "\u{2212}3h".into())
|
||||
);
|
||||
assert_eq!(against(now, &home, &home), ("Today", "Same time".into()));
|
||||
let late: Timestamp = "2026-09-28T23:30:00Z".parse().expect("a moment");
|
||||
assert_eq!(
|
||||
against(late, &zone("Pacific/Honolulu"), &zone("Asia/Tokyo")),
|
||||
("Yesterday", "\u{2212}19h".into())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod clock;
|
||||
mod stopwatch;
|
||||
#[cfg(all(test, feature = "test-support"))]
|
||||
mod tests;
|
||||
|
||||
pub use clock::{Clock, WorldClock};
|
||||
pub use stopwatch::Stopwatch;
|
||||
@@ -0,0 +1,234 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, 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, 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.
|
||||
#[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();
|
||||
if run.read(cx).since.is_some() && 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 (running, elapsed, laps) = {
|
||||
let run = run.read(cx);
|
||||
(run.since.is_some(), run.elapsed(now), run.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 now = cx.background_executor().now();
|
||||
run.update(cx, |run, cx| {
|
||||
edit(run, now);
|
||||
log::info!("stopwatch: {name} at {}", tenths(run.elapsed(now)));
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
};
|
||||
let rows = laps.iter().enumerate().rev().map(|(ix, total)| {
|
||||
let before = ix.checked_sub(1).map_or(Duration::ZERO, |last| laps[last]);
|
||||
div()
|
||||
.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(tenths(*total - before)),
|
||||
)
|
||||
.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),
|
||||
false => act("start", Run::start),
|
||||
}),
|
||||
)
|
||||
.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())),
|
||||
),
|
||||
)
|
||||
.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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, Context, FocusHandle, IntoElement, KeyUpEvent, Keystroke, ParentElement, Render,
|
||||
Styled, TestAppContext, VisualTestContext, Window, div, px,
|
||||
};
|
||||
use jiff::{Timestamp, tz::TimeZone};
|
||||
|
||||
use super::{Clock, Stopwatch, WorldClock};
|
||||
use crate::{
|
||||
primitives::{FocusNext, FocusPrev, FocusScope},
|
||||
theme::Theme,
|
||||
};
|
||||
|
||||
struct Stage {
|
||||
root: FocusHandle,
|
||||
part: fn() -> AnyElement,
|
||||
}
|
||||
|
||||
impl Render for Stage {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
FocusScope::new(&self.root)
|
||||
.root()
|
||||
.size_full()
|
||||
.child(div().w(px(400.0)).child((self.part)()))
|
||||
}
|
||||
}
|
||||
|
||||
fn settle(cx: &mut VisualTestContext) {
|
||||
for _ in 0..3 {
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, _| window.refresh());
|
||||
}
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn stage(part: fn() -> AnyElement, cx: &mut TestAppContext) -> &mut VisualTestContext {
|
||||
cx.update(|cx| {
|
||||
Theme::init(cx);
|
||||
cx.bind_keys([
|
||||
gpui::KeyBinding::new("tab", FocusNext, None),
|
||||
gpui::KeyBinding::new("shift-tab", FocusPrev, None),
|
||||
]);
|
||||
});
|
||||
let (view, cx) = cx.add_window_view(|_, cx| Stage {
|
||||
root: cx.focus_handle(),
|
||||
part,
|
||||
});
|
||||
let root = view.read_with(cx, |stage, _| stage.root.clone());
|
||||
cx.update(|window, _| window.focus(&root));
|
||||
settle(cx);
|
||||
cx
|
||||
}
|
||||
|
||||
fn press(key: &str, cx: &mut VisualTestContext) {
|
||||
cx.simulate_keystrokes(key);
|
||||
settle(cx);
|
||||
cx.simulate_event(KeyUpEvent {
|
||||
keystroke: Keystroke::parse(key).expect("a key"),
|
||||
});
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
fn wait(ms: u64, cx: &mut VisualTestContext) {
|
||||
cx.executor().advance_clock(Duration::from_millis(ms));
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
fn shown(selector: &'static str, cx: &mut VisualTestContext) -> bool {
|
||||
cx.debug_bounds(selector).is_some()
|
||||
}
|
||||
|
||||
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.
|
||||
#[gpui::test]
|
||||
fn a_stopwatch_runs_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");
|
||||
press("enter", cx);
|
||||
wait(2_000, cx);
|
||||
assert!(
|
||||
shown("stopwatch-00:01.2", cx) && !shown("stopwatch-00:03.2", 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"
|
||||
);
|
||||
}
|
||||
|
||||
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");
|
||||
div()
|
||||
.child(
|
||||
Clock::new("clock")
|
||||
.zone(zone("Europe/Paris"))
|
||||
.now(now)
|
||||
.label("Paris"),
|
||||
)
|
||||
.child(
|
||||
WorldClock::new(
|
||||
"world",
|
||||
[
|
||||
("Tokyo", zone("Asia/Tokyo")),
|
||||
("Kolkata", zone("Asia/Kolkata")),
|
||||
],
|
||||
)
|
||||
.home(zone("America/New_York"))
|
||||
.now(now),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// 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);
|
||||
wait(5_000, cx);
|
||||
}
|
||||
Reference in new issue
Block a user