guido 0.7.0

A reactive Rust GUI library using wgpu for Wayland layer shell widgets
Documentation
//! The four lines every integration test writes before it can ask anything.
//!
//! A widget cannot be laid out, pointed at or painted without a [`Tree`] to
//! hold its bounds, and registering one takes the same shape every time:
//! register, register its children, lay it out under some constraints. Five
//! test files had written that out privately before this existed, and each had
//! drifted — one dispatched inside a snapshot zone and the others did not,
//! which decides whether the non-reactive-read diagnostic fires on a hit test.
//!
//! What is *not* here is anything a single file needs: a scrollbar's handle
//! node, a caption's painted top, the colour of a named text. Those stay where
//! they are asked about. This is the surface underneath them.
//!
//! Below `Harness`, and for the same reason, is the rule that decides whether
//! a run may rewrite a reference file. It is not about widgets at all; it is
//! here because both reference harnesses have to read the same copy of it, and
//! the last time it lived in two places it lived in neither.
//!
//! Each test binary compiles its own copy — that is what `mod common;` does —
//! so a helper only one file uses is dead code in the others.
#![allow(dead_code)]

use guido::layout::{Constraints, Size};
use guido::prelude::*;
use guido::renderer::{DrawCommand, RenderNode};
use guido::tree::{Tree, WidgetId};
use guido::widgets::widget::EventResponse;

/// The Material elevation ladder, written out as the six `Shadow` values the
/// library's own table used to produce for levels 0 to 5.
///
/// It lives here, in a test file, because that is where it belongs now: guido
/// ships `shadow` and no ladder, so a design system's steps are values the
/// application writes down. Two reference harnesses draw this one, and the
/// numbers are exact rather than approximate so the pixels and the snapshot
/// text did not move when the table was deleted.
pub const LADDER: [Shadow; 6] = [
    Shadow::none(),
    Shadow::new((0.0, 1.0), 3.0, 0.0, Color::rgba(0.0, 0.0, 0.0, 0.12)),
    Shadow::new((0.0, 2.0), 4.0, 0.0, Color::rgba(0.0, 0.0, 0.0, 0.16)),
    Shadow::new((0.0, 3.0), 6.0, 0.0, Color::rgba(0.0, 0.0, 0.0, 0.19)),
    Shadow::new((0.0, 4.0), 8.0, 0.0, Color::rgba(0.0, 0.0, 0.0, 0.20)),
    Shadow::new((0.0, 6.0), 10.0, 0.0, Color::rgba(0.0, 0.0, 0.0, 0.22)),
];

/// A registered widget and the tree that holds its bounds.
pub struct Harness {
    pub tree: Tree,
    pub root: WidgetId,
}

impl Harness {
    /// Register `widget` as the root, and its children with it.
    pub fn new(widget: impl Widget + 'static) -> Self {
        let mut tree = Tree::new();
        let root = tree.register(Box::new(widget));
        tree.with_widget_mut(root, |w, id, t| w.register_children(t, id));
        Self { tree, root }
    }

    /// Register a widget and lay it out at once, which is what a test that
    /// then asks a question about it wants.
    pub fn laid_out(widget: impl Widget + 'static, width: f32, height: f32) -> Self {
        let mut surface = Self::new(widget);
        surface.lay_out(width, height);
        surface
    }

    /// Register a widget, lay it out, and put the keyboard on it.
    ///
    /// The focus belongs to the thread, not to the harness, and the test
    /// runner reuses threads — so the field a previous test focused would
    /// otherwise answer for this one. Clearing first is part of the setup
    /// rather than something each file remembers.
    pub fn focused(widget: impl Widget + 'static, width: f32, height: f32) -> Self {
        guido::reactive::focus::clear_focus();
        let harness = Self::laid_out(widget, width, height);
        guido::reactive::focus::request_focus(&harness.tree, harness.root);
        harness
    }

    /// Lay out loose within `width` x `height`.
    pub fn lay_out(&mut self, width: f32, height: f32) -> Size {
        let root = self.root;
        self.tree
            .layout_widget(root, Constraints::new(0.0, 0.0, width, height))
            .expect("the root is registered")
    }

    /// Deliver one event.
    ///
    /// Inside a snapshot zone, because that is where the real loop dispatches
    /// (`render_surface`): hit testing reads animated values for *this* event
    /// and must not subscribe to them. Outside one those reads trip the
    /// non-reactive-read diagnostic, which would be the harness reporting on
    /// itself.
    pub fn send(&mut self, event: Event) -> EventResponse {
        let root = self.root;
        guido::reactive::diagnostics::snapshot_zone(|| {
            self.tree
                .with_widget_mut(root, |w, id, t| w.event(t, id, &event))
                .expect("the root is registered")
        })
    }

    /// Deliver one event, at a named instant.
    pub fn send_at(&mut self, event: Event, at: std::time::Instant) -> EventResponse {
        self.tree.set_event_instant(Some(at));
        let response = self.send(event);
        self.tree.set_event_instant(None);
        response
    }

    /// A press and the release that follows it, at the same place.
    pub fn click(&mut self, x: f32, y: f32) {
        self.send(Event::mouse_down(x, y, MouseButton::Left));
        self.send(Event::mouse_up(x, y, MouseButton::Left));
    }

    /// Every rectangle the tree painted, in the order it drew them.
    ///
    /// What a caret or a selection highlight is made of: `TextInput` draws
    /// both as rounded rects with no radius, and four files ask about them.
    pub fn painted_rects(&mut self) -> Vec<Rect> {
        self.painted_rounded_rects()
            .into_iter()
            .map(|(rect, _)| rect)
            .collect()
    }

    /// The same rectangles with what they are filled with, for the tests that
    /// ask which of them is the caret and which is the selection band.
    pub fn painted_rounded_rects(&mut self) -> Vec<(Rect, Color)> {
        fn collect(node: &RenderNode, out: &mut Vec<(Rect, Color)>) {
            for cmd in &node.commands {
                if let DrawCommand::RoundedRect { rect, color, .. } = &**cmd {
                    out.push((*rect, *color));
                }
            }
            for child in &node.children {
                collect(child, out);
            }
        }

        let mut out = Vec::new();
        collect(&self.paint(), &mut out);
        out
    }

    /// Paint the whole tree and hand back what it drew.
    ///
    /// Inside a frame, because a paint is one of the three passes a frame is
    /// made of: a widget that asks the time while drawing — a scrollbar's
    /// hover expansion does — is asking about this frame, and the loop always
    /// has an answer. Without one it gets the wall clock and a diagnostic.
    pub fn paint(&mut self) -> RenderNode {
        let root = self.root;
        self.tree.set_frame_instant(Some(std::time::Instant::now()));
        let mut node = RenderNode::new(root.as_u64());
        self.tree.paint_widget(root, &mut node);
        self.tree.set_frame_instant(None);
        node
    }

    /// One animation pass, at a named instant, post-order as the loop runs it.
    pub fn advance(&mut self, at: std::time::Instant) {
        self.tree.set_frame_instant(Some(at));
        for id in self.tree.collect_subtree_post_order(self.root) {
            self.tree
                .with_widget_mut(id, |w, id, t| w.advance_animations(t, id));
        }
        self.tree.set_frame_instant(None);
    }
}

/// What to do with a reference file this run disagrees with.
///
/// Two variables, and the difference between them is the whole rule.
/// `UPDATE_*` makes a reference that is not there yet: a new scenario needs a
/// first picture and that is ordinary work. `REBLESS_*` rewrites one that is,
/// which turns a failing test green without changing anything back — so it is
/// a decision, taken with the diff in front of somebody, and the pull request
/// carries the `golden-update` label to say it was taken.
///
/// One copy, read by both `assert_snapshot` and `assert_golden`, because the
/// last time the rule lived in two places it lived in neither: both harnesses
/// wrote unconditionally on `UPDATE_*` while their own module headers said
/// they declined, and `REBLESS_*` was read nowhere at all.
///
/// A pure function of three booleans, so the table below is the test.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Blessing {
    /// Write the reference, and skip the comparison.
    Write,
    /// Compare against what is on disk, whatever was asked for.
    Compare,
}

/// What to do, given what the environment asked for and whether the
/// reference is already on disk.
pub fn blessing(update: bool, rebless: bool, exists: bool) -> Blessing {
    match (update, rebless, exists) {
        // Rewriting is what REBLESS is, and the only thing it is.
        (_, true, _) => Blessing::Write,
        // A reference that is not there yet is not a rewrite.
        (true, false, false) => Blessing::Write,
        // Asked to create one that already exists: decline, and let the
        // comparison report what actually moved. This is the line that was
        // missing.
        (true, false, true) => Blessing::Compare,
        (false, false, _) => Blessing::Compare,
    }
}

/// Read the pair for one kind of reference — `"SNAPSHOTS"` or `"GOLDEN"`.
///
/// Says so when it declines. Somebody who asked to bless and got silence would
/// read the pass that follows as the blessing having happened.
pub fn blessing_from_env(kind: &str, exists: bool) -> Blessing {
    let asked = |prefix: &str| std::env::var_os(format!("{prefix}_{kind}")).is_some();
    let update = asked("UPDATE");
    let decision = blessing(update, asked("REBLESS"), exists);
    if update && decision == Blessing::Compare {
        eprintln!(
            "UPDATE_{kind} declined: this reference already exists, and rewriting one \
             makes a failing test pass without changing anything back. Read the diff \
             the comparison is about to print; if the change is intended, that is \
             REBLESS_{kind}=1 and the `golden-update` label on the pull request."
        );
    }
    decision
}

/// Write the reference at `path`, if the rule allows it. Returns whether it
/// wrote, so the caller can fall through to the comparison when it did not.
///
/// The writing is here rather than at the two call sites so that "decided it
/// may write" and "wrote" cannot come apart: a harness that consults the rule
/// and then writes anyway is the defect this all exists to stop, and it would
/// read as compliant at both call sites.
pub fn write_if_blessed(path: &std::path::Path, blessing: Blessing, write: impl FnOnce()) -> bool {
    match blessing {
        Blessing::Write => {
            write();
            assert!(
                path.exists(),
                "blessed {} and it is still not there",
                path.display()
            );
            true
        }
        Blessing::Compare => false,
    }
}

/// One scripted play at a time in this binary.
///
/// The heap counter a benchmark run reports is the whole process's, because a
/// frame's allocations are — wgpu's threads and the graphics driver's allocate
/// inside a frame as surely as the frame loop does. So two tests playing
/// scripts on two threads would each be charged the other's frames. The lock
/// is per binary, which is what is wanted: a binary is a process.
///
/// Poison is stepped over rather than propagated. It means an earlier test
/// panicked, which is already a failure; making every later test fail too
/// hides which one it was.
pub fn one_play_at_a_time() -> std::sync::MutexGuard<'static, ()> {
    static PLAYING: std::sync::Mutex<()> = std::sync::Mutex::new(());
    PLAYING
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
}

/// How close two plays have to come on a count of allocations: a thousandth.
///
/// The tight one, and it holds. Nine runs of one benchmark spread by about ten
/// allocations in five hundred thousand, and forty runs of the test binary with
/// four copies of itself competing for the machine did not move it at all —
/// a request is a request whoever is making it. The regression that made this
/// counter worth having was 4034 in 533865.
pub const COUNTS_AGREE_TO: u64 = 1000;

/// And on a count of bytes: a twentieth.
///
/// Far looser, because bytes are not requests. The number of requests inside a
/// frame is fixed by the script, but their *sizes* are not all the code's: on a
/// loaded machine the same play came back with 2258089 bytes and 2279145, a
/// spread of 0.93%, and `static_clip` moved 1.3% — a driver sizing a buffer for
/// itself, in steps. A tighter margin failed one run in four under load, which
/// is a test asserting something its figure does not do.
///
/// It is still worth asserting. The change that made this section worth
/// printing cut a benchmark's bytes by 51%.
pub const BYTES_AGREE_TO: u64 = 20;

/// How close two plays of one script have to come on a heap figure: within one
/// part in `in_parts`, or sixteen, whichever is larger.
///
/// Not exactly, which is the difference between these and the counts beside
/// them. The counter is process-wide by design, so a play carries whatever the
/// driver's own threads did inside it — see [`COUNTS_AGREE_TO`] and
/// [`BYTES_AGREE_TO`] for what that costs each kind of figure. The wider of the
/// two windows the benchmarks print takes in the adapter's own setup as well,
/// so its count of allocations is held to a hundredth rather than a thousandth.
pub fn heap_figures_agree(first: u64, second: u64, what: &str, in_parts: u64) {
    let margin = (first.max(second) / in_parts).max(16);
    assert!(
        first.abs_diff(second) <= margin,
        "the same script over the same tree asked the allocator for {what} \
         {first} one run and {second} the next, which is further apart than the \
         {margin} the driver's own threads account for"
    );
}

/// An application driven without a compositor, or `None` where this machine
/// has no adapter to draw with.
///
/// The skip and the variable that forbids it are the contract the rasterizer
/// job depends on — in that job a skip is a failure, and `GUIDO_GPU_REQUIRED`
/// is how it says so. `tests/headless_app.rs` and `tests/external_widget.rs`
/// each still carry their own copy; they do not take `mod common`.
#[cfg(feature = "testing")]
pub fn headless() -> Option<guido::testing::Headless> {
    match guido::testing::Headless::new() {
        Some(app) => Some(app),
        None if std::env::var_os("GUIDO_GPU_REQUIRED").is_some() => {
            panic!("GUIDO_GPU_REQUIRED is set and no GPU adapter was found")
        }
        None => {
            eprintln!("no GPU adapter; skipping");
            None
        }
    }
}