pub struct Window { /* private fields */ }Expand description
A native window hosting a SwiftUI view built from Rust.
The root view can be replaced at any time with Window::set_root, which is
how a Rust-driven interface updates after it is already on screen.
Implementations§
Source§impl Window
impl Window
Sourcepub fn new(view: &ViewHandle, title: &str, width: f64, height: f64) -> Self
pub fn new(view: &ViewHandle, title: &str, width: f64, height: f64) -> Self
Create and show a window. Does not start the event loop.
Examples found in repository?
examples/hot_reload.rs (line 362)
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 // Pixels differing *above* the status strip.
371 //
372 // The strip reports the generation, so it changes on every reload whether
373 // or not the screen did — comparing whole frames would make "the screen
374 // survived a file that does not parse" indistinguishable from "the screen
375 // changed".
376 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 // Measured, not estimated: with a 420-point capture the strip's own
384 // pixels start at row 294, so everything from 290 down is excluded.
385 // Guessing 90 left part of the strip in the comparison and reported a
386 // screen that had not moved as having moved.
387 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 // Let the window settle before the first capture, or it is caught blank.
397 swiftui_native::render::pump_events(1.0);
398 let (first_path, first) = shot("1_start");
399
400 // A user changes the toggle. Everything after this checks it stays
401 // changed, which is what separates reloading from restarting.
402 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 /// One edit, and whether the screen should follow it.
408 struct Edit {
409 name: &'static str,
410 src: &'static str,
411 /// `false` for a spec that does not parse: the screen must *not*
412 /// change, because the last good one has to stay put.
413 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 // Mid-edit: a brace that is never closed. The screen must survive.
435 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 // …and the toggle is still on, after every reload and a broken file.
476 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 // `Window::run` enters AppKit's loop and never comes back, which leaves
509 // nowhere to notice the file changed. Pumping the loop in slices instead
510 // keeps the window live *and* gives this thread a turn between them —
511 // `state::refresh` has to happen on the main thread, so the watcher only
512 // raises a flag and the work is done here.
513 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 // Two mechanisms, because the screen may be either shape: the
518 // synthesised root re-evaluates its body on `refresh`, and
519 // `set_root` hands SwiftUI a fresh tree to diff against. The first
520 // alone leaves a structural change — a `VStack` becoming a
521 // `ZStack` — with nothing to diff.
522 swiftui_native::state::refresh();
523 window.set_root(&content_view());
524 eprintln!("reloaded (generation {})", GENERATION.load(Ordering::Relaxed));
525 }
526 }
527}Sourcepub fn set_root(&self, view: &ViewHandle)
pub fn set_root(&self, view: &ViewHandle)
Replace the displayed view. SwiftUI diffs it against the previous tree.
Examples found in repository?
examples/hot_reload.rs (line 404)
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 // Pixels differing *above* the status strip.
371 //
372 // The strip reports the generation, so it changes on every reload whether
373 // or not the screen did — comparing whole frames would make "the screen
374 // survived a file that does not parse" indistinguishable from "the screen
375 // changed".
376 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 // Measured, not estimated: with a 420-point capture the strip's own
384 // pixels start at row 294, so everything from 290 down is excluded.
385 // Guessing 90 left part of the strip in the comparison and reported a
386 // screen that had not moved as having moved.
387 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 // Let the window settle before the first capture, or it is caught blank.
397 swiftui_native::render::pump_events(1.0);
398 let (first_path, first) = shot("1_start");
399
400 // A user changes the toggle. Everything after this checks it stays
401 // changed, which is what separates reloading from restarting.
402 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 /// One edit, and whether the screen should follow it.
408 struct Edit {
409 name: &'static str,
410 src: &'static str,
411 /// `false` for a spec that does not parse: the screen must *not*
412 /// change, because the last good one has to stay put.
413 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 // Mid-edit: a brace that is never closed. The screen must survive.
435 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 // …and the toggle is still on, after every reload and a broken file.
476 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 // `Window::run` enters AppKit's loop and never comes back, which leaves
509 // nowhere to notice the file changed. Pumping the loop in slices instead
510 // keeps the window live *and* gives this thread a turn between them —
511 // `state::refresh` has to happen on the main thread, so the watcher only
512 // raises a flag and the work is done here.
513 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 // Two mechanisms, because the screen may be either shape: the
518 // synthesised root re-evaluates its body on `refresh`, and
519 // `set_root` hands SwiftUI a fresh tree to diff against. The first
520 // alone leaves a structural change — a `VStack` becoming a
521 // `ZStack` — with nothing to diff.
522 swiftui_native::state::refresh();
523 window.set_root(&content_view());
524 eprintln!("reloaded (generation {})", GENERATION.load(Ordering::Relaxed));
525 }
526 }
527}Sourcepub fn hosting(&self) -> &HostingView
pub fn hosting(&self) -> &HostingView
The hosting view, for rendering or further configuration.
Examples found in repository?
examples/hot_reload.rs (line 365)
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 // Pixels differing *above* the status strip.
371 //
372 // The strip reports the generation, so it changes on every reload whether
373 // or not the screen did — comparing whole frames would make "the screen
374 // survived a file that does not parse" indistinguishable from "the screen
375 // changed".
376 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 // Measured, not estimated: with a 420-point capture the strip's own
384 // pixels start at row 294, so everything from 290 down is excluded.
385 // Guessing 90 left part of the strip in the comparison and reported a
386 // screen that had not moved as having moved.
387 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 // Let the window settle before the first capture, or it is caught blank.
397 swiftui_native::render::pump_events(1.0);
398 let (first_path, first) = shot("1_start");
399
400 // A user changes the toggle. Everything after this checks it stays
401 // changed, which is what separates reloading from restarting.
402 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 /// One edit, and whether the screen should follow it.
408 struct Edit {
409 name: &'static str,
410 src: &'static str,
411 /// `false` for a spec that does not parse: the screen must *not*
412 /// change, because the last good one has to stay put.
413 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 // Mid-edit: a brace that is never closed. The screen must survive.
435 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 // …and the toggle is still on, after every reload and a broken file.
476 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}pub fn as_ptr(&self) -> *mut c_void
Auto Trait Implementations§
impl !Freeze for Window
impl !RefUnwindSafe for Window
impl !Send for Window
impl !Sync for Window
impl Unpin for Window
impl UnsafeUnpin for Window
impl UnwindSafe for Window
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more