mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 10:27:44 -06:00
Web: use the browser clock everywhere on wasm (insert weekday, PDF dates, layout timing, UI timers) — follow-up to #214 (#230)
#214 gave now_iso the browser's clock on wasm; the other clock helpers were still stubs there: Insert Date's weekday was always Thursday, the PDF creation date was left out, and the layout and UI millisecond clocks returned 0, so browser AutoSave's timer never fired, the caret didn't blink and status messages never cleared. The engine's clock is now one now_unix() (std on native, js_sys::Date in the browser, as #214 does) that now_iso and the weekday share; the pdf, layout and ui-egui clocks read js_sys::Date the same way on wasm. Refs #188. Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Dmitrii Khlapov
Claude Opus 5.5
parent
4c4dc14a54
commit
302528d67e
Generated
+3
@@ -4994,6 +4994,7 @@ dependencies = [
|
||||
name = "wordcraft-layout"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5026,6 +5027,7 @@ dependencies = [
|
||||
"hayro-interpret",
|
||||
"hayro-syntax",
|
||||
"image",
|
||||
"js-sys",
|
||||
"krilla",
|
||||
"kurbo",
|
||||
"log",
|
||||
@@ -5065,6 +5067,7 @@ dependencies = [
|
||||
"egui",
|
||||
"egui_kittest",
|
||||
"image",
|
||||
"js-sys",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -449,20 +449,8 @@ pub fn format_date(fmt: &str) -> String {
|
||||
const MONTHS: [&str; 12] =
|
||||
["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
||||
const DAYS: [&str; 7] = ["Thursday", "Friday", "Saturday", "Sunday", "Monday", "Tuesday", "Wednesday"];
|
||||
let days_since = {
|
||||
// Days since epoch for weekday.
|
||||
let secs = {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
0u64
|
||||
}
|
||||
};
|
||||
(secs / 86_400) as usize
|
||||
};
|
||||
// Days since the epoch, for the weekday.
|
||||
let days_since = (super::now_unix() / 86_400) as usize;
|
||||
let month = MONTHS.get(m.saturating_sub(1)).copied().unwrap_or("January");
|
||||
let day = DAYS.get(days_since % 7).copied().unwrap_or("Monday");
|
||||
let mut out = String::new();
|
||||
|
||||
@@ -254,28 +254,32 @@ pub fn split_para(s: &mut Session, at: &Pos) -> Result<Pos, CmdError> {
|
||||
}
|
||||
|
||||
/// ISO-8601 timestamp (UTC, seconds) for `secs` since the Unix epoch.
|
||||
fn iso_from_unix_secs(secs: u64) -> String {
|
||||
pub fn iso_from_unix_secs(secs: u64) -> String {
|
||||
let (y, m, d) = civil_from_days((secs / 86_400) as i64);
|
||||
let t = secs % 86_400;
|
||||
format!("{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z", t / 3600, t / 60 % 60, t % 60)
|
||||
}
|
||||
|
||||
/// ISO-8601 timestamp (UTC, seconds).
|
||||
pub fn now_iso() -> String {
|
||||
/// Seconds since the Unix epoch (UTC): the system clock, or the browser's on the web.
|
||||
pub fn now_unix() -> u64 {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
|
||||
iso_from_unix_secs(secs)
|
||||
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
|
||||
}
|
||||
// `SystemTime::now()` panics on wasm32-unknown-unknown, so ask the browser's clock.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let ms = js_sys::Date::now();
|
||||
// Clamp a NaN or pre-epoch clock to 0 rather than fail.
|
||||
iso_from_unix_secs(if ms.is_finite() && ms > 0.0 { (ms / 1000.0) as u64 } else { 0 })
|
||||
if ms.is_finite() && ms > 0.0 { (ms / 1000.0) as u64 } else { 0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// ISO-8601 timestamp (UTC, seconds).
|
||||
pub fn now_iso() -> String {
|
||||
iso_from_unix_secs(now_unix())
|
||||
}
|
||||
|
||||
/// Days since 1970-01-01 → (year, month, day) (Howard Hinnant's algorithm).
|
||||
pub fn civil_from_days(z: i64) -> (i64, u32, u32) {
|
||||
let z = z + 719_468;
|
||||
|
||||
@@ -1638,3 +1638,17 @@ fn styles_apply_rejected_on_protected_document() {
|
||||
assert_eq!(para_style(&s, 0), None);
|
||||
assert_eq!(s.undo_labels(), undo);
|
||||
}
|
||||
|
||||
/// Comments, revisions and dates read the real clock (#188: the web stamped 2026-01-01).
|
||||
#[test]
|
||||
fn timestamps_come_from_the_clock() {
|
||||
let before = cmd::now_unix();
|
||||
assert!(before > 1_760_000_000, "{before}");
|
||||
let mut s = s();
|
||||
run(&mut s, "text.insert", json!({"text": "Dated"}));
|
||||
run(&mut s, "select.text", json!({"text": "Dated"}));
|
||||
run(&mut s, "review.newComment", json!({"text": "When?"}));
|
||||
let date = run(&mut s, "review.comments", json!({}))[0]["date"].as_str().unwrap_or_default().to_string();
|
||||
// Fixed-width ISO strings sort by time.
|
||||
assert!(date >= cmd::iso_from_unix_secs(before) && date <= cmd::iso_from_unix_secs(cmd::now_unix()), "{date}");
|
||||
}
|
||||
|
||||
@@ -19,3 +19,7 @@ unicode-segmentation = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# The browser clock (`SystemTime::now()`/`Instant::now()` panic on wasm32).
|
||||
js-sys = "0.3"
|
||||
|
||||
@@ -1617,16 +1617,18 @@ pub fn is_floating(o: &InlineObject) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Milliseconds since an arbitrary epoch (monotonic on native; 0 on wasm).
|
||||
/// Milliseconds since an arbitrary epoch (monotonic on native; the browser's clock on the web).
|
||||
pub fn now_ms() -> f64 {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
static T0: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
|
||||
T0.get_or_init(std::time::Instant::now).elapsed().as_secs_f64() * 1000.0
|
||||
}
|
||||
// `Instant::now()` panics on wasm32-unknown-unknown, so ask the browser's clock.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
0.0
|
||||
let ms = js_sys::Date::now();
|
||||
if ms.is_finite() { ms } else { 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ image = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# The browser clock (`SystemTime::now()` panics on wasm32).
|
||||
js-sys = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
hayro-syntax = "0.7.2"
|
||||
hayro-interpret = { version = "0.7.0", default-features = false, features = ["embed-fonts"] }
|
||||
|
||||
+13
-8
@@ -259,15 +259,20 @@ fn parse_iso(s: &str) -> Option<Civil> {
|
||||
Some((y, mo as u8, d as u8, h as u8, mi as u8, sec as u8))
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// The system clock, or the browser's on the web.
|
||||
fn now_unix() -> Option<i64> {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs() as i64)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn now_unix() -> Option<i64> {
|
||||
None
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let secs = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
|
||||
i64::try_from(secs).ok()
|
||||
}
|
||||
// `SystemTime::now()` panics on wasm32-unknown-unknown, so ask the browser's clock.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let ms = js_sys::Date::now();
|
||||
(ms.is_finite() && ms > 0.0).then(|| (ms / 1000.0) as i64)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unix seconds (UTC) → civil time (proleptic Gregorian).
|
||||
|
||||
@@ -23,6 +23,10 @@ log = { workspace = true }
|
||||
# The system's preferred interface languages (#8). Native: OS APIs; wasm: none (English).
|
||||
sys-locale = "0.3"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# The browser clock (`SystemTime::now()`/`Instant::now()` panic on wasm32).
|
||||
js-sys = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
|
||||
image = { workspace = true }
|
||||
|
||||
@@ -890,15 +890,18 @@ fn discards_document(id: &str, params: &Value) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wall-clock milliseconds since the Unix epoch: the system clock, or the browser's on the web.
|
||||
pub fn now_ms() -> f64 {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64() * 1000.0).unwrap_or(0.0)
|
||||
}
|
||||
// `SystemTime::now()` panics on wasm32-unknown-unknown, so ask the browser's clock.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
0.0
|
||||
let ms = js_sys::Date::now();
|
||||
if ms.is_finite() && ms > 0.0 { ms } else { 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user