Skip to main content

Window

Struct Window 

Source
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

Source

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}
Source

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}
Source

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}
Source

pub fn as_ptr(&self) -> *mut c_void

Source

pub fn run(&self) -> !

Enter the AppKit event loop. Never returns.

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> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.