guido 0.12.0

A reactive Rust GUI library using wgpu for Wayland layer shell widgets
Documentation
//! Everything the application keeps on its thread, in one place.
//!
//! These were twenty-two separate `thread_local!` cells, and `App::drop` reset
//! them by calling a hand-written list of nine functions. The list was wrong:
//! `DEFAULT_FONT_FAMILY` was not on it, so a second `App` on the same thread
//! inherited the font the first one declared. Nothing failed to build, because
//! nothing collected the cells anywhere that could be counted.
//!
//! One struct behind one thread-local fixes that at the definition: [`reset`]
//! destructures it, so a field added here and forgotten there is a compile
//! error rather than a value the next `App` inherits.
//!
//! **The fields keep their own cells; the struct is not itself a `RefCell`.**
//! Wrapping the whole of it would serialise state that is already interleaved
//! — updating the widget-ref registry writes signals, and a signal write
//! queues a job — so one borrow would be held across a call that takes
//! another, and the application would panic where it now runs. This is
//! floem's shape for the same reason: its `Runtime` is a plain struct of
//! cells behind one thread-local.
//!
//! The line this draws is what the state is *for*, not which module it was
//! written in: everything the running application has queued, mirrored or
//! declared lives here, and the machinery that makes a reactive closure
//! reactive at all is its twin, `ReactiveState` in `src/reactive/state.rs`.
//! So the clipboard queues and the cursor come here from `src/reactive/`,
//! and `invalidation.rs` keeps its subscriber registry over there while its
//! dirtied segments, which are a queue the reconciler drains, come here. The
//! diagnostics stay out of both: they outlive an `App` on purpose.

use std::cell::{Cell, RefCell};
use std::sync::Arc;

use rustc_hash::{FxHashMap, FxHashSet};
use smallvec::SmallVec;

use crate::deferred::{DeferredQueue, DeferredSlot};
use crate::image_decode::{DecodeEntry, DecodeKey, Decoder, ImageEvent};
use crate::jobs::{JobQueues, ScheduledJob};
use crate::reactive::clipboard::{PasteTarget, PastedText, SelectionKind};
use crate::reactive::cursor::CursorIcon;
use crate::reactive::timer::Timers;
use crate::reactive::{OwnerId, Prop};
use crate::renderer::TextMeasurer;
use crate::session_lock::{LockData, LockRequest};
use crate::surface::{SurfaceCommand, SurfaceId};
use crate::tree::WidgetId;
use crate::widget_ref::WidgetRef;
use crate::widgets::font::{FontFamily, FontWeight};

thread_local! {
    static APP: AppState = AppState::default();
}

/// Reach the application's state. Every one of the fields below is read and
/// written through here and nowhere else.
pub(crate) fn with_app_state<R>(f: impl FnOnce(&AppState) -> R) -> R {
    APP.with(f)
}

/// The application's state, for as long as one `App` lives on this thread.
#[derive(Default)]
pub(crate) struct AppState {
    // --- What the loop will be asked for on its next iteration -------------
    /// Widget jobs, queued by signal writes that have no `Tree`, sorted per
    /// surface by `distribute_jobs`.
    pub(crate) pending_jobs: RefCell<JobQueues>,
    /// Jobs owed to a clock (the caret), queued from widget code that has no
    /// loop. Outside `pending_jobs` so `has_runnable_jobs` stays "there is work
    /// for *this* frame": treating a scheduled job as pending is what turns a
    /// blink into a poll.
    pub(crate) scheduled_jobs: RefCell<Vec<ScheduledJob>>,
    /// `set_timeout` and `set_interval`'s callbacks, waiting for their pass.
    pub(crate) timers: RefCell<Timers>,
    /// Dynamic-children segments a write dirtied, waiting for the
    /// reconciliation that has the tree.
    pub(crate) dirty_segments: RefCell<FxHashMap<WidgetId, SmallVec<[u32; 4]>>>,
    /// `SurfaceHandle` calls from application code, which has no platform.
    pub(crate) surface_commands: DeferredQueue<SurfaceCommand>,
    /// A shape waiting for the loop to hand it to the compositor. Being empty
    /// *is* the "nothing to sync" state — there is no dirty flag beside any of
    /// these four to keep in step with the value.
    pub(crate) outgoing_cursor: DeferredSlot<CursorIcon>,
    /// A copy waiting for the loop to hand it to the compositor.
    pub(crate) outgoing_clipboard: DeferredSlot<String>,
    /// The primary-selection counterpart of `outgoing_clipboard`.
    pub(crate) outgoing_primary: DeferredSlot<String>,
    /// A focus request from application code, which has no tree, parked until
    /// one is laid out. One slot, not a queue: two requests in a frame are two
    /// answers to "where should the keyboard be", and the last one is meant.
    pub(crate) pending_focus: RefCell<Option<WidgetRef>>,
    /// The widget each surface had focused when the focus went to another
    /// surface, by that surface's root, and whether Tab had put it there. Given
    /// back when the keyboard returns to the surface (#726), as is an
    /// `autofocus` field that appeared there while the focus was elsewhere and
    /// waits for the keyboard (#731). An entry exists only while the focus is
    /// somewhere else.
    pub(crate) kept_focus: RefCell<FxHashMap<WidgetId, (WidgetId, bool)>>,
    /// Pastes asked for, waiting for the loop to start their reads.
    pub(crate) paste_requests: DeferredQueue<(SelectionKind, PasteTarget)>,
    /// Pastes whose text has arrived, waiting for the loop to hand them over.
    pub(crate) pastes: DeferredQueue<(PasteTarget, PastedText)>,
    /// A lock or unlock asked for by application code, waiting for the loop.
    pub(crate) lock_request: DeferredSlot<LockRequest>,

    // --- What the platform last said, and what we last told it -------------
    /// The last shape handed to the loop, so resolving the same one again
    /// sends nothing. A pointer entering a surface sends it anyway, through
    /// `resend_cursor`.
    pub(crate) current_cursor: Cell<CursorIcon>,
    /// The declaration the pointer last came to rest over, so the same one
    /// again — every move across a button — is recognised as nothing new.
    pub(crate) pointed_cursor: Cell<Prop<CursorIcon>>,
    /// The scope of the effect watching `pointed_cursor` when it is a signal,
    /// disposed when the pointer finds another declaration.
    pub(crate) cursor_watch: Cell<Option<OwnerId>>,
    /// Whether the compositor offers a clipboard to paste: only that an offer
    /// exists, because nothing is read until a paste asks for it.
    pub(crate) clipboard_offered: Cell<bool>,
    /// The reads started for pastes, each with the token its answer comes back
    /// under and everyone waiting on it.
    pub(crate) paste_reads: RefCell<Vec<(u64, Vec<PasteTarget>)>>,
    /// The token the next read is given.
    pub(crate) next_paste_read: Cell<u64>,
    /// The lock-screen factory and each output's lock surface while locked.
    pub(crate) lock: RefCell<LockData>,
    /// The popups that are still open. A `PopupHandle` is `Copy` and outlives
    /// its popup, so the registry is the only truth about whether it is.
    pub(crate) live_popups: RefCell<FxHashSet<SurfaceId>>,
    /// Which widget each `WidgetRef` points at: application code names a
    /// widget with one before any `WidgetId` exists.
    pub(crate) widget_refs: RefCell<FxHashMap<WidgetId, WidgetRef>>,

    // --- Fonts -------------------------------------------------------------
    /// One shaping cache for every `layout` that measures text, none of which
    /// is handed one. Built on first use — see `with_measurer`.
    pub(crate) text_measurer: RefCell<Option<TextMeasurer>>,
    /// Read by every text widget at construction, before any tree or surface
    /// exists.
    pub(crate) default_font_family: Cell<FontFamily>,
    /// Font bytes loaded before any renderer exists, handed to each font
    /// system when it is built.
    pub(crate) custom_fonts: RefCell<Vec<Arc<Vec<u8>>>>,
    /// Which of those were already loaded, so loading twice is idempotent.
    pub(crate) custom_font_hashes: RefCell<FxHashSet<u64>>,
    /// The face each declared family and weight is drawn in, and its line
    /// height (`renderer::text`). The faces are fixed once the first font
    /// system takes `custom_fonts`, and finding one is a shaping.
    pub(crate) resolved_fonts:
        RefCell<FxHashMap<(FontFamily, FontWeight), crate::renderer::ResolvedFont>>,
    /// Whether a font system already took the list, so a late load can say it
    /// came too late.
    pub(crate) fonts_consumed: Cell<bool>,

    // --- Images ------------------------------------------------------------
    /// One entry per raster source being shown, each holding the signal its
    /// decode is written through — read by every image widget's paint, none of
    /// which is handed the others. See `src/image_decode.rs`.
    pub(crate) decoded_images: RefCell<FxHashMap<DecodeKey, DecodeEntry>>,
    /// The thread those decodes run on, spawned by the first one. Forgetting it
    /// closes its channel, which is what ends the thread.
    pub(crate) image_decoder: RefCell<Option<Decoder>>,
    /// What the renderer and the image widgets reported about those entries —
    /// a texture missing or evicted, an entry let go — for the loop to settle
    /// where a signal may be written.
    pub(crate) image_events: DeferredQueue<ImageEvent>,
    /// The bytes of texture the renderer's image cache may hold before it
    /// evicts, as the application set it. `None` is
    /// [`DEFAULT_IMAGE_CACHE_BUDGET`](crate::DEFAULT_IMAGE_CACHE_BUDGET).
    pub(crate) image_cache_budget: Cell<Option<usize>>,
}

/// Forget everything this `App` put here, so the next one on this thread
/// starts clean.
///
/// Destructured rather than written as a list of statements: a field left out
/// below is an unused binding, and one added above and not bound at all is a
/// missing-field error. Both are what `-D warnings` and the compiler catch,
/// and neither is what the hand-written list this replaces could catch.
pub(crate) fn reset() {
    with_app_state(|app| {
        let AppState {
            pending_jobs,
            scheduled_jobs,
            timers,
            dirty_segments,
            surface_commands,
            outgoing_cursor,
            outgoing_clipboard,
            outgoing_primary,
            pending_focus,
            kept_focus,
            paste_requests,
            pastes,
            lock_request,
            current_cursor,
            pointed_cursor,
            cursor_watch,
            clipboard_offered,
            paste_reads,
            next_paste_read,
            lock,
            live_popups,
            widget_refs,
            text_measurer,
            default_font_family,
            custom_fonts,
            custom_font_hashes,
            resolved_fonts,
            fonts_consumed,
            decoded_images,
            image_decoder,
            image_events,
            image_cache_budget,
        } = app;

        // `take` rather than a value per line: the struct derives `Default`,
        // so what a field starts an `App` with and what it is given back here
        // are the same expression, and neither can drift from the other.
        // Timer ids alone survive so kept handles cannot alias the next app.
        //
        // What holds an application closure goes first. A queued surface
        // command carries a widget factory, a lock request carries a
        // lock-screen one, a timer and a paste their callbacks, and dropping any drops what it captured — a
        // `ChildrenSource` among them, whose own `Drop` queues a job. So the
        // queues those land in are emptied below, after everything that can
        // still push into them.
        surface_commands.clear();
        let successor = timers.borrow().successor();
        drop(timers.replace(successor));
        lock_request.clear();
        paste_requests.clear();
        paste_reads.take();
        pastes.clear();
        lock.take();
        widget_refs.take();
        text_measurer.take();

        pending_jobs.take();
        scheduled_jobs.take();
        dirty_segments.take();
        outgoing_cursor.clear();
        outgoing_clipboard.clear();
        outgoing_primary.clear();
        pending_focus.take();
        kept_focus.take();

        current_cursor.take();
        pointed_cursor.take();
        cursor_watch.take();
        clipboard_offered.take();
        next_paste_read.take();
        live_popups.take();

        default_font_family.take();
        custom_fonts.take();
        custom_font_hashes.take();
        resolved_fonts.take();
        fonts_consumed.take();

        image_events.clear();
        decoded_images.take();
        image_decoder.take();
        image_cache_budget.take();
    });
}