lib.rs raw

//! A widget an *application* implements, for the test suite: the far side of
//! the registry seam.
//!
//! This crate stands in for the crate an application writes its own widgets
//! in. It matters that it is a separate crate rather than a type in the test
//! binary: the proc-macro cannot see another crate's items, which is the whole
//! reason a `.gdw` manifest declares the interface and carries `(type "…")`
//! verbatim for rustc to resolve. A fixture living inside the consumer would
//! quietly test something easier than the real thing.
//!
//! It authors its own interface in `widgets/note.gdw` and registers the Rust
//! side from it with [`register_widget!`], below — one manifest, read here to
//! generate the runtime factory and by the application's `include_component!`
//! to type-check a `.gdc` that writes the widget. The tests that drive it are
//! `crates/guiduck/tests/m17_user_widget.rs`.

use guiduck_core::event::{EventData, EventKind, PointerButton, PointerEvent, UserValue};
use guiduck_core::style::ComputedStyle;
use guiduck_core::text::TextContext;
use guiduck_core::{Dirt, Widget, register_widget, taffy};
use guiduck_scene::Fragment;
use guiduck_scene::geom::{Rect, Size};
use guiduck_scene::paint::Brush;

// The Rust side of `widgets/note.gdw`: a constructor and one type-checked
// setter thunk per declared prop, submitted to the link-time registry. The
// setter calls it generates (`set_title`, `set_zoom`) are checked against the
// methods below, so a manifest that named a setter `NoteView` lacks would not
// compile here.
register_widget!(NoteView, "widgets/note.gdw");

/// A plain leaf whose size follows its props and whose fill comes from a theme
/// token.
///
/// It owes the registry exactly the contract a `.gdw` promises — `Default +
/// Widget`, plus one setter per declared prop — and nothing else. Nothing here
/// knows that `.gdc` files exist.
#[derive(Default)]
pub struct NoteView {
    title: String,
    zoom: f32,
    ink: Option<Brush>,
    dirt: Dirt,
    emitted: Vec<EventData>,
}

impl NoteView {
    /// `(prop title String)`. Layout-affecting, so the setter marks layout
    /// dirt — the classification lives with the property, as it does for
    /// every builtin.
    pub fn set_title(&mut self, title: String) {
        if self.title == title {
            return;
        }
        self.title = title;
        self.dirt.mark_layout();
    }

    /// `(prop scale f32 :setter set_zoom)` — a setter that deliberately does
    /// not follow the `set_<prop>` convention, so the manifest has to name it.
    pub fn set_zoom(&mut self, zoom: f32) {
        if self.zoom == zoom {
            return;
        }
        self.zoom = zoom;
        self.dirt.mark_layout();
    }

    pub fn title(&self) -> &str {
        &self.title
    }

    pub fn zoom(&self) -> f32 {
        self.zoom
    }

    pub fn ink(&self) -> Option<&Brush> {
        self.ink.as_ref()
    }
}

impl Widget for NoteView {
    fn measure(
        &mut self,
        _text: &mut TextContext,
        _known: taffy::Size<Option<f32>>,
        _available: taffy::Size<taffy::AvailableSpace>,
    ) -> taffy::Size<f32> {
        // Deliberately derived from both props: a `:title` that did not land,
        // or a `:scale` that never re-applied, changes the layout — so the
        // compiled-vs-interpreted differential can see it.
        taffy::Size {
            width: self.title.len() as f32 * 4.0 * self.zoom,
            height: 8.0 * self.zoom,
        }
    }

    fn paint(&mut self, fragment: &mut Fragment, size: Size) {
        if let Some(ink) = &self.ink {
            fragment.fill(
                Rect::from_origin_size((0.0, 0.0), size).to_rounded_rect(0.0),
                ink.clone(),
            );
        }
    }

    /// The name a theme selector and a `.gdc` write. The style engine matches
    /// on this string, so an application's widget is themable through the one
    /// mechanism every builtin uses — nothing special-cases it.
    fn type_name(&self) -> &'static str {
        "note"
    }

    fn apply_style(&mut self, style: &ComputedStyle) {
        if let Some(ink) = style.brush("ink")
            && self.ink.as_ref() != Some(ink)
        {
            self.ink = Some(ink.clone());
            self.dirt.mark_paint();
        }
    }

    fn on_pointer(
        &mut self,
        kind: EventKind,
        event: &PointerEvent,
        _text: &mut TextContext,
        _clipboard: &mut dyn guiduck_core::Clipboard,
    ) -> bool {
        if kind != EventKind::Click {
            return false;
        }
        // The widget's half of the manifest's event contract: the names and
        // payload types it declared. A left click opens, delivering the note's
        // title; a right click asks to be dismissed and carries nothing.
        self.emitted.push(match event.button {
            Some(PointerButton::Right) => EventData::User {
                name: "on-dismiss".to_owned(),
                payload: None,
            },
            _ => EventData::User {
                name: "on-open".to_owned(),
                payload: Some(UserValue::Str(self.title.clone())),
            },
        });
        true
    }

    fn take_dirt(&mut self) -> Dirt {
        std::mem::take(&mut self.dirt)
    }

    fn take_emitted(&mut self) -> Vec<EventData> {
        std::mem::take(&mut self.emitted)
    }
}