//! 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, dirt: Dirt, emitted: Vec, } 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_` 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>, _available: taffy::Size, ) -> taffy::Size { // 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 { std::mem::take(&mut self.emitted) } }