Skip to main content

pump_events

Function pump_events 

Source
pub fn pump_events(seconds: f64)
Expand description

Run a real event loop for seconds, dequeuing and dispatching NSEvents.

pump turns the run loop, which is enough for SwiftUI’s clock and for display, but it never touches the application’s event queue — so an event posted with postEvent:atStart: just sits there and a gesture never fires. This is the loop that delivers them.

Use pump for anything that only needs time to pass; use this when something has to receive input.

Examples found in repository?
examples/hot_reload.rs (line 397)
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}