1#![allow(dead_code)]
27
28use std::sync::atomic::{AtomicU64, Ordering};
29use std::sync::{Mutex, OnceLock};
30use std::time::{Duration, SystemTime};
31use swiftui_native::prelude::*;
32
33#[path = "common/spec.rs"]
34mod spec;
35
36fn path() -> String {
39 std::env::args()
40 .skip(1)
41 .find(|a| !a.starts_with("--"))
42 .unwrap_or_else(|| format!("{}/../../target/hot_reload.ui", env!("CARGO_MANIFEST_DIR")))
43}
44
45const STARTER: &str = r#"// Edit this file and save. The window follows — and the
46// controls below keep their values across the reload.
47// Names: vstack hstack zstack text spacer divider button color image
48// rect circle capsule progress gauge group_box list scroll form label
49
50vstack spacing=14 pad=24 align=leading {
51 text "Hot reload" style=large_title bold
52 text "Change this file; the app does not restart." color=secondary size=13
53 divider
54 hstack spacing=10 {
55 circle color=green frame=12x12
56 text "watching" size=13
57 spacer
58 text "edit me" color=blue size=13
59 }
60
61 // `@name` binds a control to a slot that outlives the view tree. Change
62 // these, then edit anything above, and they keep what you set.
63 toggle "Dark mode" @dark
64 stepper "Copies" @copies min=0 max=20
65 slider @level min=0 max=100
66
67 spacer
68}
69"#;
70
71#[derive(Default)]
79struct Reloader {
80 last_good: String,
81 error: Option<String>,
82 generation: u64,
83 micros: u64,
85}
86
87fn state_cell() -> &'static Mutex<Reloader> {
88 static CELL: OnceLock<Mutex<Reloader>> = OnceLock::new();
89 CELL.get_or_init(|| {
90 Mutex::new(Reloader { last_good: STARTER.to_string(), ..Default::default() })
91 })
92}
93
94static GENERATION: AtomicU64 = AtomicU64::new(0);
95
96fn body() -> View {
97 let (src, error, generation, micros) = {
98 let g = state_cell().lock().expect("state");
99 (g.last_good.clone(), g.error.clone(), g.generation, g.micros)
100 };
101
102 let screen = spec::parse(&src)
106 .and_then(|nodes| spec::build(&nodes))
107 .unwrap_or_else(|e| text(&e).fg_system(SystemColor::Red).padding(20.0));
108
109 vstack_spacing(&[screen, status(error.as_deref(), generation, micros)], 0.0)
110}
111
112fn status(error: Option<&str>, generation: u64, micros: u64) -> View {
118 let slots = spec::slot_summary();
119 let slot_line = if slots.is_empty() {
120 String::new()
121 } else {
122 slots
123 .iter()
124 .map(|(k, v)| format!("{k}={v}"))
125 .collect::<Vec<_>>()
126 .join(" ")
127 };
128
129 let rows = vec![
135 divider(),
136 hstack_spacing(
137 &[
138 circle_shape()
139 .fill_system(if error.is_some() {
140 SystemColor::Orange
141 } else {
142 SystemColor::Clear
143 })
144 .frame(8.0, 8.0),
145 text(error.unwrap_or(""))
146 .font_size(11.0)
147 .fg_system(SystemColor::Orange),
148 spacer(),
149 ],
150 6.0,
151 )
152 .padding(6.0)
153 .frame(348.0, 24.0),
154 hstack_spacing(
155 &[
156 circle_shape()
157 .fill_system(if error.is_some() {
158 SystemColor::Orange
159 } else {
160 SystemColor::Green
161 })
162 .frame(8.0, 8.0),
163 text(&format!("gen {generation}"))
164 .font_size(11.0)
165 .fg_system(SystemColor::Secondary),
166 text(&if micros < 1000 {
169 format!("{micros} \u{b5}s")
170 } else {
171 format!("{:.1} ms", micros as f64 / 1000.0)
172 })
173 .font_size(11.0)
174 .fg_system(SystemColor::Secondary),
175 spacer(),
176 text(&slot_line).font_size(11.0).fg_system(SystemColor::Secondary),
177 ],
178 8.0,
179 )
180 .padding(6.0)
181 .frame(348.0, 24.0),
182 ];
183 vstack_spacing(&rows, 0.0)
184}
185
186fn content_view() -> swiftui_native::ViewHandle {
187 swiftui_native::state::root_view(|_| body().into_handle())
188}
189
190fn reload(p: &str) -> bool {
205 let started = std::time::Instant::now();
206 let Ok(text) = std::fs::read_to_string(p) else { return false };
207
208 let mut g = state_cell().lock().expect("state");
209 if g.last_good == text && g.error.is_none() {
210 return false;
211 }
212 match spec::parse(&text).and_then(|nodes| spec::build(&nodes)) {
213 Ok(_) => {
214 g.last_good = text;
215 g.error = None;
216 }
217 Err(e) => {
218 if g.error.as_deref() == Some(e.as_str()) {
221 return false;
222 }
223 g.error = Some(e);
224 }
225 }
226 g.generation += 1;
227 g.micros = started.elapsed().as_micros() as u64;
228 GENERATION.store(g.generation, Ordering::Relaxed);
229 true
230}
231
232const SETTLE: Duration = Duration::from_millis(60);
236
237fn watch(p: String) {
254 std::thread::spawn(move || {
255 let mut seen: Option<(SystemTime, u64)> = None;
256 let mut settle_at: Option<std::time::Instant> = None;
257 loop {
258 let stamp = std::fs::metadata(&p)
259 .and_then(|m| Ok((m.modified()?, m.len())))
260 .ok();
261 match (stamp, seen) {
262 (None, _) => {}
264 (Some(now), Some(before)) if now == before => {}
265 (Some(now), _) => {
266 seen = Some(now);
267 settle_at = Some(std::time::Instant::now() + SETTLE);
268 }
269 }
270 if let Some(at) = settle_at {
271 if std::time::Instant::now() >= at {
272 settle_at = None;
273 if reload(&p) {
274 PENDING.store(true, Ordering::Relaxed);
275 }
276 }
277 }
278 std::thread::sleep(Duration::from_millis(30));
279 }
280 });
281}
282
283static PENDING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
284
285fn frames() {
291 let p = path();
292 let edits: &[(&str, &str)] = &[
293 ("1_start", STARTER),
294 (
295 "2_recoloured",
296 r#"vstack spacing=14 pad=24 align=leading {
297 text "Hot reload" style=large_title bold color=blue
298 text "Same process. Second version." color=secondary size=13
299 divider
300 hstack spacing=10 {
301 circle color=orange frame=12x12
302 text "reloaded once" size=13
303 spacer
304 }
305 progress 0.6
306 spacer
307}"#,
308 ),
309 (
310 "3_restructured",
311 r#"// A different shape entirely: the Swift type changes with it.
312zstack align=center frame=320x220 {
313 color #24303f frame=300x200 corner=18
314 vstack spacing=12 pad=20 align=leading {
315 text "Third version" style=title2 bold color=white
316 text "No recompile happened between these frames." color=white size=12 opacity=0.75
317 spacer
318 gauge 0.8 label="done"
319 }
320}"#,
321 ),
322 ("4_broken", "vstack { text \"unclosed"),
323 ];
324
325 for (name, text_src) in edits {
326 std::fs::write(&p, text_src).expect("write spec");
327 reload(&p);
330 let handle = content_view();
331 let out = format!("{}/../../target/hot_reload_{name}.png", env!("CARGO_MANIFEST_DIR"));
332 let ok = swiftui_native::render::render(&handle, 360, 400).write_png(&out);
333 let built = body().into_handle();
334 println!(
335 "{name:16} {}",
336 swiftui_native::diag::type_name(swiftui_native::views::meta_of(&built))
337 .chars()
338 .take(72)
339 .collect::<String>()
340 );
341 println!("{:16} {}", "", if ok { out } else { "render failed".into() });
342 }
343 std::fs::write(&p, STARTER).expect("restore spec");
346}
347
348#[cfg(target_os = "macos")]
356fn drive() {
357 let p = path();
358 std::fs::write(&p, STARTER).expect("write starter");
359 reload(&p);
360 watch(p.clone());
361
362 let window = swiftui_native::Window::new(&content_view(), "Hot reload", 360.0, 420.0);
363 let shot = |n: &str| {
364 let out = format!("{}/../../target/hot_reload_live_{n}.png", env!("CARGO_MANIFEST_DIR"));
365 let r = swiftui_native::render::render_hosting(window.hosting(), 360, 420);
366 r.write_png(&out);
367 (out, r)
368 };
369
370 fn screen_diff(
377 a: &swiftui_native::render::Rendered,
378 b: &swiftui_native::render::Rendered,
379 ) -> u32 {
380 if a.width != b.width || a.height != b.height {
381 return u32::MAX;
382 }
383 const SCREEN_ROWS: u32 = 290;
388 let keep = (SCREEN_ROWS.min(a.height) * a.width * 4) as usize;
389 a.pixels[..keep]
390 .chunks_exact(4)
391 .zip(b.pixels[..keep].chunks_exact(4))
392 .filter(|(x, y)| x != y)
393 .count() as u32
394 }
395
396 swiftui_native::render::pump_events(1.0);
398 let (first_path, first) = shot("1_start");
399
400 spec::set_flag("dark", true);
403 swiftui_native::state::refresh();
404 window.set_root(&content_view());
405 swiftui_native::render::pump_events(0.4);
406
407 struct Edit {
409 name: &'static str,
410 src: &'static str,
411 expect_change: bool,
414 }
415
416 let edits = &[
417 Edit {
418 name: "2_edited",
419 src: "vstack spacing=10 pad=20 align=leading {\n \
420 text \"Edited while running\" style=title bold color=red\n \
421 text \"the process never restarted\" color=secondary size=12\n \
422 toggle \"Dark mode\" @dark\n \
423 gauge 0.9 label=\"reloaded\"\n}",
424 expect_change: true,
425 },
426 Edit {
427 name: "3_restructured",
428 src: "zstack align=center frame=300x200 {\n \
429 color #223344 frame=280x180 corner=14\n \
430 text \"a different shape entirely\" color=white size=14\n}",
431 expect_change: true,
432 },
433 Edit {
434 name: "4_broken",
436 src: "zstack align=center frame=300x200 {\n color #223344",
437 expect_change: false,
438 },
439 ];
440
441 let mut shots = vec![(first_path, first)];
442 let mut ok = true;
443 for e in edits {
444 std::fs::write(&p, e.src).expect("write edit");
445 let deadline = std::time::Instant::now() + Duration::from_secs(4);
446 while !PENDING.load(Ordering::Relaxed) && std::time::Instant::now() < deadline {
447 swiftui_native::render::pump_events(0.05);
448 }
449 if !PENDING.swap(false, Ordering::Relaxed) {
450 eprintln!("{}: the watcher never fired", e.name);
451 std::process::exit(1);
452 }
453 swiftui_native::state::refresh();
454 window.set_root(&content_view());
455 swiftui_native::render::pump_events(0.6);
456 let (path_now, shot_now) = shot(e.name);
457
458 let (prev_path, prev) = shots.last().expect("a previous shot");
459 let d = screen_diff(prev, &shot_now);
460 println!("{prev_path}\n -> {path_now}\n {d} pixels differ above the status strip");
461 match (e.expect_change, d) {
462 (true, 0) => {
463 eprintln!(" the window did not follow the edit");
464 ok = false;
465 }
466 (false, n) if n > 0 => {
467 eprintln!(" a file that does not parse replaced the screen");
468 ok = false;
469 }
470 _ => {}
471 }
472 shots.push((path_now, shot_now));
473 }
474
475 let dark = spec::slot_summary().into_iter().find(|(k, _)| k == "dark").map(|(_, v)| v);
477 println!("slot `dark` after every reload: {dark:?}");
478 if dark.as_deref() != Some("true") {
479 eprintln!(" a reload reset the slot");
480 ok = false;
481 }
482
483 std::fs::write(&p, STARTER).expect("restore spec");
484 if !ok {
485 std::process::exit(1);
486 }
487 println!("followed every valid edit, survived an invalid one, kept its state");
488}
489
490#[cfg(target_os = "macos")]
491fn main() {
492 let p = path();
493 if std::env::args().any(|a| a == "--frames") {
494 return frames();
495 }
496 if std::env::args().any(|a| a == "--drive") {
497 return drive();
498 }
499
500 if !std::path::Path::new(&p).exists() {
501 std::fs::write(&p, STARTER).expect("write starter");
502 eprintln!("wrote {p}");
503 }
504 reload(&p);
505 watch(p.clone());
506 eprintln!("watching {p} — edit it and save");
507
508 let window = swiftui_native::Window::new(&content_view(), "Hot reload", 360.0, 420.0);
514 loop {
515 swiftui_native::render::pump_events(0.05);
516 if PENDING.swap(false, Ordering::Relaxed) {
517 swiftui_native::state::refresh();
523 window.set_root(&content_view());
524 eprintln!("reloaded (generation {})", GENERATION.load(Ordering::Relaxed));
525 }
526 }
527}
528
529#[cfg(not(target_os = "macos"))]
530fn main() {
531 let p = path();
532 reload(&p);
533 watch(p);
534 swiftui_native::app::run(content_view)
535}