lib.rs
raw
//! 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<u8> {
static FONT_BLOB: std::sync::OnceLock<Blob<u8>> = 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<f32>,
) -> Layout<guiduck_scene::paint::Brush> {
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"
);
}
}