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