//! Sample scenes exercising the display list, shared between the runnable //! examples and the golden-image tests so both always render the same //! content. //! //! Text uses only an embedded DejaVu Sans (no system fonts), keeping rendered //! output identical across machines. use guiduck_core::text::TextContext; use guiduck_scene::geom::{Affine, BezPath, Point, Rect, Shape as _, Size, Stroke}; use guiduck_scene::paint::color::palette::css; use guiduck_scene::paint::{ BlendMode, Blob, Compose, Fill, Gradient, ImageAlphaType, ImageBrush, ImageData, ImageFormat, Mix, }; use guiduck_scene::{Fragment, FragmentId, FragmentStore}; use parley::{Alignment, AlignmentOptions, FontFamily, Layout, StyleProperty}; pub mod counter; pub mod stress; pub mod widgets; /// The embedded font family's name as registered with fontique. pub const SAMPLE_FONT_FAMILY: &str = "DejaVu Sans"; static SAMPLE_FONT: &[u8] = include_bytes!("../fonts/DejaVuSans.ttf"); /// The embedded sample font as a shareable blob. One process-wide blob: /// resource identity matters (blobs compare by id), so every text context /// built from this font produces comparable `FontData`. pub fn sample_font() -> Blob { static FONT_BLOB: std::sync::OnceLock> = std::sync::OnceLock::new(); FONT_BLOB .get_or_init(|| Blob::new(std::sync::Arc::new(SAMPLE_FONT))) .clone() } /// A text context that sees only the embedded sample font. pub fn sample_text_context() -> TextContext { TextContext::hermetic([sample_font()]) } /// Text stack for sample scenes: an embedded-font-only parley context. pub struct Samples { text: TextContext, } impl Samples { pub fn new() -> Self { Self { text: sample_text_context(), } } /// Lay out a paragraph in the embedded font, wrapped to `max_width` /// logical pixels. pub fn layout_text( &mut self, text: &str, font_size: f32, brush: guiduck_scene::paint::Brush, max_width: Option, ) -> Layout { let mut builder = self.text .layout_cx .ranged_builder(&mut self.text.font_cx, text, 1.0, true); builder.push_default(StyleProperty::FontFamily(FontFamily::named( SAMPLE_FONT_FAMILY, ))); builder.push_default(StyleProperty::FontSize(font_size)); builder.push_default(StyleProperty::Brush(brush)); let mut layout = builder.build(text); layout.break_all_lines(max_width); layout.align(Alignment::Start, AlignmentOptions::default()); layout } /// The full showcase scene: shapes, strokes, gradients, clips, layers, /// an image, transforms, a child fragment, and wrapped text. pub fn showcase(&mut self, store: &mut FragmentStore, viewport: Size) -> FragmentId { let mut root = Fragment::new(); let width = viewport.width; // Title, wrapped to the viewport so resizing reflows it. let title = self.layout_text( "guiduck — one scene, two renderers. \ The quick brown fox jumps over the lazy dog. 0123456789", 18.0, css::BLACK.into(), Some((width - 32.0).max(50.0) as f32), ); guiduck_core::text::append_layout(&mut root, &title, Point::new(16.0, 16.0)); // Solid and gradient fills. root.fill(Rect::new(16.0, 96.0, 116.0, 156.0), css::REBECCA_PURPLE); root.fill( Rect::new(132.0, 96.0, 232.0, 156.0).to_rounded_rect(12.0), Gradient::new_linear((132.0, 96.0), (232.0, 156.0)) .with_stops([css::ORANGE, css::CRIMSON]), ); root.fill( Rect::new(248.0, 96.0, 308.0, 156.0).to_rounded_rect(30.0), Gradient::new_radial((278.0, 126.0), 30.0).with_stops([css::WHITE, css::STEEL_BLUE]), ); // Dashed stroke and a self-intersecting star filled even-odd. let mut dashed = Stroke::new(3.0); dashed.dash_pattern = [8.0, 4.0].as_slice().into(); root.stroke( Rect::new(324.0, 96.0, 424.0, 156.0).to_rounded_rect(8.0), css::DARK_GREEN, dashed, ); root.fill_with_rule( pentagram(Point::new(490.0, 126.0), 38.0), css::GOLDENROD, Fill::EvenOdd, ); // Clip: diagonal stripes visible only inside a rounded rect. root.push_clip(Rect::new(16.0, 180.0, 216.0, 260.0).to_rounded_rect(16.0)); for i in 0..20 { let x = 16.0 + i as f64 * 12.0; root.push_transform(Affine::translate((x, 180.0)) * Affine::skew(0.5, 0.0)); root.fill(Rect::new(0.0, 0.0, 6.0, 80.0), css::TEAL); root.pop_transform(); } root.pop_clip(); // Layer: overlapping circles composited at half opacity with multiply. root.fill(circle(Point::new(280.0, 220.0), 36.0), css::TOMATO); root.push_layer( 0.5, BlendMode::new(Mix::Multiply, Compose::SrcOver), Rect::new(232.0, 180.0, 380.0, 260.0), ); root.fill(circle(Point::new(320.0, 220.0), 36.0), css::CORNFLOWER_BLUE); root.pop_layer(); // Procedural checkerboard image, scaled into its destination. root.image(checkerboard(), Rect::new(408.0, 180.0, 528.0, 260.0)); // A child fragment placed twice with different transforms, proving // fragment composition and reuse. let badge = store.insert(badge_fragment()); root.child(Affine::translate((16.0, 284.0)), badge); root.child( Affine::translate((160.0, 284.0)) * Affine::rotate(0.35) * Affine::scale(0.8), badge, ); // Bottom-right anchor proving resize handling. root.fill( Rect::new( viewport.width - 40.0, viewport.height - 40.0, viewport.width - 16.0, viewport.height - 16.0, ) .to_rounded_rect(6.0), css::MEDIUM_VIOLET_RED, ); store.insert(root) } } impl Default for Samples { fn default() -> Self { Self::new() } } /// A small fragment used as a reusable child in sample scenes. fn badge_fragment() -> Fragment { let mut badge = Fragment::new(); badge.fill( Rect::new(0.0, 0.0, 120.0, 48.0).to_rounded_rect(24.0), Gradient::new_linear((0.0, 0.0), (120.0, 48.0)) .with_stops([css::SLATE_BLUE, css::DARK_ORCHID]), ); badge.stroke( Rect::new(2.0, 2.0, 118.0, 46.0).to_rounded_rect(22.0), css::WHITE, Stroke::new(2.0), ); badge } fn circle(center: Point, radius: f64) -> guiduck_scene::Shape { guiduck_scene::Shape::Path(guiduck_scene::geom::Circle::new(center, radius).to_path(0.1)) } /// A pentagram drawn by connecting alternate vertices of a pentagon. The /// strokes cross each other, so an even-odd fill leaves the central pentagon /// empty while non-zero would fill it — making the fill rule visible. fn pentagram(center: Point, radius: f64) -> BezPath { let mut path = BezPath::new(); let vertex = |i: u32| { let angle = (i * 2) as f64 * (2.0 * std::f64::consts::PI / 5.0) - std::f64::consts::FRAC_PI_2; Point::new( center.x + radius * angle.cos(), center.y + radius * angle.sin(), ) }; path.move_to(vertex(0)); for i in 1..5 { path.line_to(vertex(i)); } path.close_path(); path } /// An 8x8 checkerboard as image data. pub(crate) fn checkerboard() -> ImageBrush { const CELLS: u32 = 8; const CELL_PX: u32 = 4; const SIZE: u32 = CELLS * CELL_PX; let mut data = Vec::with_capacity((SIZE * SIZE * 4) as usize); for y in 0..SIZE { for x in 0..SIZE { let on = ((x / CELL_PX) + (y / CELL_PX)) % 2 == 0; let [r, g, b] = if on { [30, 30, 30] } else { [220, 220, 220] }; data.extend_from_slice(&[r, g, b, 255]); } } ImageBrush::from(ImageData { data: Blob::new(std::sync::Arc::new(data)), format: ImageFormat::Rgba8, alpha_type: ImageAlphaType::Alpha, width: SIZE, height: SIZE, }) } #[cfg(test)] mod tests { use super::*; #[test] fn showcase_builds_balanced_fragments() { let mut samples = Samples::new(); let mut store = FragmentStore::new(); let root = samples.showcase(&mut store, Size::new(560.0, 360.0)); let fragment = store.get(root).unwrap(); assert!(fragment.is_balanced()); assert!(!fragment.items.is_empty()); } #[test] fn text_layout_uses_embedded_font() { let mut samples = Samples::new(); let layout = samples.layout_text("hello", 16.0, css::BLACK.into(), None); assert!(layout.width() > 0.0); let mut fragment = Fragment::new(); guiduck_core::text::append_layout(&mut fragment, &layout, Point::ORIGIN); assert!( fragment .items .iter() .any(|i| matches!(i, guiduck_scene::DisplayItem::GlyphRun(_))), "expected at least one glyph run" ); } }