use frust::authoring::Role;
use frust::authoring::text::{
FontFamily, FontWeight, GenericSlot, TextContext, TextLayout, TextStyle,
};
use frust::authoring::{
BoxConstraints, BuildCtx, ChangeFlags, LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View,
Widget,
};
use frust::{ShapeScale, StatusPalette, Theme};
use kurbo::{Point, Rect, RoundedRect, Shape, Size, Vec2};
use peniko::{Brush, Color};
const PATH_TOLERANCE: f64 = 0.1;
const BADGE_PAD_X: f64 = 9.0;
const BADGE_PAD_Y: f64 = 4.0;
const BADGE_GAP: f64 = 6.0;
const BADGE_DOT_SIZE: f64 = 5.0;
const BADGE_FONT_SIZE: f32 = 10.5;
const BADGE_LETTER_SPACING: f32 = BADGE_FONT_SIZE * 0.03;
const BADGE_BORDER_WIDTH: f64 = 1.0;
const BADGE_BORDER_ALPHA: f32 = 0.3;
const BADGE_ACCENT_WASH_ALPHA: f32 = 0.12;
const BADGE_SUCCESS_FG: Color = Color::from_rgb8(0x5f, 0xd8, 0x8f);
const BADGE_SUCCESS_BG: Color = Color::from_rgb8(0x1f, 0x31, 0x30);
const BADGE_WARNING_FG: Color = Color::from_rgb8(0xf5, 0xc8, 0x60);
const BADGE_WARNING_BG: Color = Color::from_rgb8(0x31, 0x2f, 0x2a);
const BADGE_WARNING_BORDER: Color = Color::from_rgb8(0xf5, 0xc8, 0x60);
const BADGE_ERROR_FG: Color = Color::from_rgb8(0xff, 0x6b, 0x6b);
const BADGE_ERROR_BG: Color = Color::from_rgb8(0x32, 0x24, 0x2c);
const BADGE_INFO_FG: Color = Color::from_rgb8(0x5e, 0xc8, 0xd8);
const BADGE_INFO_BG: Color = Color::from_rgb8(0x20, 0x32, 0x3c);
const BADGE_NEUTRAL_FG: Color = Color::from_rgb8(0xa3, 0x9c, 0x88);
const BADGE_NEUTRAL_BG: Color = Color::from_rgb8(0x1e, 0x23, 0x30);
const BADGE_NEUTRAL_BORDER: Color = Color::from_rgb8(0x3e, 0x3f, 0x44);
const BADGE_ACCENT_FG: Color = Color::from_rgb8(0xff, 0xb6, 0x27);
fn with_alpha(color: Color, alpha: f32) -> Color {
let c = color.components;
Color::new([c[0], c[1], c[2], alpha])
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BadgeVariant {
Success,
Warning,
Error,
Info,
Neutral,
Accent,
}
pub struct BadgeView {
label: String,
variant: BadgeVariant,
dot: bool,
}
pub fn badge(label: impl Into<String>, variant: BadgeVariant) -> BadgeView {
BadgeView {
label: label.into(),
variant,
dot: false,
}
}
impl BadgeView {
pub fn dot(mut self, dot: bool) -> Self {
self.dot = dot;
self
}
}
struct GlyphLabel {
content: String,
layout: Option<TextLayout>,
laid_out_style: Option<TextStyle>,
laid_out_max_width: Option<f32>,
}
impl GlyphLabel {
fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
layout: None,
laid_out_style: None,
laid_out_max_width: None,
}
}
fn set_content(&mut self, content: impl Into<String>) {
let content = content.into();
if self.content != content {
self.content = content;
self.layout = None;
}
}
fn layout(&mut self, ctx: &mut LayoutCtx, style: &TextStyle, max_width: Option<f32>) -> Size {
if let Some(cached) = &self.layout
&& self.laid_out_max_width == max_width
&& self.laid_out_style.as_ref() == Some(style)
{
return cached.size();
}
let text_ctx = ctx.text_context::<TextContext>();
let laid = text_ctx.layout(&self.content, style, max_width);
let size = laid.size();
self.layout = Some(laid);
self.laid_out_style = Some(style.clone());
self.laid_out_max_width = max_width;
size
}
fn paint(&self, origin: Point, scene: &mut dyn PaintScene) {
if let Some(layout) = &self.layout {
for run in layout.to_scene_runs(origin) {
scene.draw_glyph_run(run);
}
}
}
}
pub struct BadgeWidget {
label: GlyphLabel,
label_text: String,
label_size: Size,
variant: BadgeVariant,
dot: bool,
}
impl<State: 'static> View<State> for BadgeView {
type Element = BadgeWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> BadgeWidget {
BadgeWidget {
label: GlyphLabel::new(self.label.to_lowercase()),
label_text: self.label.clone(),
label_size: Size::ZERO,
variant: self.variant,
dot: self.dot,
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut BadgeWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
if prev.label != self.label {
element.label.set_content(self.label.to_lowercase());
element.label_text = self.label.clone();
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
if prev.variant != self.variant {
element.variant = self.variant;
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
if prev.dot != self.dot {
element.dot = self.dot;
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
flags
}
}
fn ui_face() -> FontFamily {
FontFamily::stack_with_generic(["IBM Plex Mono"], GenericSlot::Monospace)
}
fn badge_label_style(theme: Option<&Theme>, color: Color) -> TextStyle {
TextStyle {
family: theme.map_or_else(ui_face, |t| t.type_scale.label_small.family.clone()),
weight: FontWeight::MEDIUM,
letter_spacing: BADGE_LETTER_SPACING,
..TextStyle::new(BADGE_FONT_SIZE, color)
}
}
fn resolve_badge_colors(
theme: Option<&Theme>,
variant: BadgeVariant,
) -> (Color, Color, Option<Color>) {
let Some(theme) = theme else {
return unthemed_badge_colors(variant);
};
let scheme = theme.scheme();
match variant {
BadgeVariant::Success => match theme.extension::<StatusPalette>() {
Some(status) => {
let c = status.colors(theme.brightness);
(c.success_container, c.success, None)
}
None => (BADGE_SUCCESS_BG, BADGE_SUCCESS_FG, None),
},
BadgeVariant::Warning => match theme.extension::<StatusPalette>() {
Some(status) => {
let c = status.colors(theme.brightness);
(
c.warning_container,
c.warning,
Some(with_alpha(c.warning, BADGE_BORDER_ALPHA)),
)
}
None => (
BADGE_WARNING_BG,
BADGE_WARNING_FG,
Some(BADGE_WARNING_BORDER),
),
},
BadgeVariant::Error => (scheme.error_container, scheme.error, None),
BadgeVariant::Info => match theme.extension::<StatusPalette>() {
Some(status) => {
let c = status.colors(theme.brightness);
(
c.info_container,
c.info,
Some(with_alpha(c.info, BADGE_BORDER_ALPHA)),
)
}
None => (BADGE_INFO_BG, BADGE_INFO_FG, Some(BADGE_INFO_FG)),
},
BadgeVariant::Neutral => (
scheme.surface_container_high,
scheme.on_surface_variant,
Some(scheme.outline),
),
BadgeVariant::Accent => (
with_alpha(scheme.primary, BADGE_ACCENT_WASH_ALPHA),
scheme.primary,
None,
),
}
}
fn unthemed_badge_colors(variant: BadgeVariant) -> (Color, Color, Option<Color>) {
match variant {
BadgeVariant::Success => (BADGE_SUCCESS_BG, BADGE_SUCCESS_FG, None),
BadgeVariant::Warning => (
BADGE_WARNING_BG,
BADGE_WARNING_FG,
Some(BADGE_WARNING_BORDER),
),
BadgeVariant::Error => (BADGE_ERROR_BG, BADGE_ERROR_FG, None),
BadgeVariant::Info => (BADGE_INFO_BG, BADGE_INFO_FG, Some(BADGE_INFO_FG)),
BadgeVariant::Neutral => (
BADGE_NEUTRAL_BG,
BADGE_NEUTRAL_FG,
Some(BADGE_NEUTRAL_BORDER),
),
BadgeVariant::Accent => (
with_alpha(BADGE_ACCENT_FG, BADGE_ACCENT_WASH_ALPHA),
BADGE_ACCENT_FG,
None,
),
}
}
fn resolve_badge_radius(theme: Option<&Theme>, size: Size) -> f64 {
match theme {
Some(theme) => ShapeScale::resolve(theme.shape.full, size.width, size.height),
None => size.height / 2.0,
}
}
impl Widget for BadgeWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let theme = Theme::from_layout_ctx(ctx);
let (_, fg, _) = resolve_badge_colors(theme, self.variant);
let style = badge_label_style(theme, fg);
let label_size = self.label.layout(ctx, &style, None);
self.label_size = label_size;
let dot_extra = if self.dot {
BADGE_DOT_SIZE + BADGE_GAP
} else {
0.0
};
let width = BADGE_PAD_X * 2.0 + dot_extra + label_size.width;
let content_height = if self.dot {
label_size.height.max(BADGE_DOT_SIZE)
} else {
label_size.height
};
let height = content_height + BADGE_PAD_Y * 2.0;
bc.constrain(Size::new(width, height))
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
let theme = Theme::from_paint_ctx(ctx);
let (bg, fg, border) = resolve_badge_colors(theme, self.variant);
let radius = resolve_badge_radius(theme, ctx.size());
scene.fill_rounded_rect(ctx.origin(), ctx.size(), radius, bg);
if let Some(border_color) = border {
let rr =
RoundedRect::from_rect(Rect::from_origin_size(Point::ORIGIN, ctx.size()), radius);
let path = rr.to_path(PATH_TOLERANCE);
scene.stroke_path(
ctx.origin(),
&path,
BADGE_BORDER_WIDTH,
&Brush::Solid(border_color),
);
}
let mut x = BADGE_PAD_X;
if self.dot {
let y = (ctx.size().height - BADGE_DOT_SIZE) / 2.0;
scene.fill_rounded_rect(
ctx.origin() + Vec2::new(x, y),
Size::new(BADGE_DOT_SIZE, BADGE_DOT_SIZE),
BADGE_DOT_SIZE / 2.0,
fg,
);
x += BADGE_DOT_SIZE + BADGE_GAP;
}
let label_y = (ctx.size().height - self.label_size.height) / 2.0;
self.label
.paint(ctx.origin() + Vec2::new(x, label_y), scene);
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
ctx.push_node(Role::Status, |node| {
node.set_label(self.label_text.as_str());
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use frust::Brightness;
use std::any::Any;
#[derive(Default)]
struct Recorder {
rrects: Vec<(Point, Size, f64, Color)>,
strokes: Vec<Color>,
glyph_colors: Vec<Color>,
}
impl PaintScene for Recorder {
fn fill_rect(&mut self, _o: Point, _s: Size, _c: Color) {}
fn draw_text(&mut self, _o: Point, _t: &str) {}
fn fill_rounded_rect(&mut self, o: Point, s: Size, radius: f64, color: Color) {
self.rrects.push((o, s, radius, color));
}
fn stroke_path(&mut self, _o: Point, _path: &kurbo::BezPath, _width: f64, brush: &Brush) {
if let Brush::Solid(c) = brush {
self.strokes.push(*c);
}
}
fn draw_glyph_run(&mut self, run: frust::authoring::scene::GlyphRun) {
if let Brush::Solid(c) = run.brush {
self.glyph_colors.push(c);
}
}
}
fn build_widget(variant: BadgeVariant, dot: bool) -> BadgeWidget {
let view = badge("Connected", variant).dot(dot);
let mut counter = 0u64;
View::<()>::build(&view, &mut BuildCtx::new(&mut counter))
}
fn badge_helper(label: &str, variant: BadgeVariant) -> BadgeView {
badge(label, variant)
}
fn layout_and_paint(widget: &mut BadgeWidget, theme: Option<&Theme>) -> Recorder {
let mut tcx = TextContext::new();
let mut lctx = match theme {
Some(t) => {
LayoutCtx::with_resources(Some(&mut tcx as &mut dyn Any), Some(t as &dyn Any))
}
None => LayoutCtx::with_text_context(&mut tcx as &mut dyn Any),
};
let size = widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(400.0, 100.0)));
let mut rec = Recorder::default();
let mut pctx = match theme {
Some(t) => PaintCtx::new(Point::ZERO, size).with_theme(t),
None => PaintCtx::new(Point::ZERO, size),
};
widget.paint(&mut pctx, &mut rec);
rec
}
#[test]
fn unthemed_success_uses_fallback_constants() {
let _ = badge_helper("x", BadgeVariant::Success); let mut w = build_widget(BadgeVariant::Success, false);
let rec = layout_and_paint(&mut w, None);
assert_eq!(rec.rrects[0].3, BADGE_SUCCESS_BG);
assert_eq!(rec.glyph_colors[0], BADGE_SUCCESS_FG);
}
#[test]
fn unthemed_warning_has_a_border() {
let mut w = build_widget(BadgeVariant::Warning, false);
let rec = layout_and_paint(&mut w, None);
assert_eq!(rec.rrects[0].3, BADGE_WARNING_BG);
assert_eq!(rec.glyph_colors[0], BADGE_WARNING_FG);
assert_eq!(rec.strokes[0], BADGE_WARNING_BORDER);
}
#[test]
fn unthemed_info_uses_fallback_constants() {
let mut w = build_widget(BadgeVariant::Info, false);
let rec = layout_and_paint(&mut w, None);
assert_eq!(rec.rrects[0].3, BADGE_INFO_BG);
assert_eq!(rec.glyph_colors[0], BADGE_INFO_FG);
assert_eq!(rec.strokes[0], BADGE_INFO_FG);
}
#[test]
fn unthemed_neutral_has_a_border() {
let mut w = build_widget(BadgeVariant::Neutral, false);
let rec = layout_and_paint(&mut w, None);
assert_eq!(rec.rrects[0].3, BADGE_NEUTRAL_BG);
assert_eq!(rec.strokes[0], BADGE_NEUTRAL_BORDER);
}
#[test]
fn unthemed_accent_is_a_true_alpha_wash() {
let mut w = build_widget(BadgeVariant::Accent, false);
let rec = layout_and_paint(&mut w, None);
assert_eq!(
rec.rrects[0].3,
with_alpha(BADGE_ACCENT_FG, BADGE_ACCENT_WASH_ALPHA)
);
assert_eq!(rec.glyph_colors[0], BADGE_ACCENT_FG);
}
#[test]
fn glyph_dark_theme_resolves_success_from_status_palette() {
let theme = crate::baseline();
let mut w = build_widget(BadgeVariant::Success, false);
let rec = layout_and_paint(&mut w, Some(&theme));
let status = theme.extension::<StatusPalette>().unwrap();
let colors = status.colors(Brightness::Dark);
assert_eq!(rec.rrects[0].3, colors.success_container);
assert_eq!(rec.glyph_colors[0], colors.success);
}
#[test]
fn glyph_light_theme_resolves_warning_from_status_palette() {
let theme = crate::baseline().with_brightness(Brightness::Light);
let mut w = build_widget(BadgeVariant::Warning, false);
let rec = layout_and_paint(&mut w, Some(&theme));
let status = theme.extension::<StatusPalette>().unwrap();
let colors = status.colors(Brightness::Light);
assert_eq!(rec.rrects[0].3, colors.warning_container);
assert_eq!(rec.glyph_colors[0], colors.warning);
assert_eq!(
rec.strokes[0],
with_alpha(colors.warning, BADGE_BORDER_ALPHA)
);
}
#[test]
fn glyph_dark_theme_resolves_info_from_status_palette() {
let theme = crate::baseline();
let mut w = build_widget(BadgeVariant::Info, false);
let rec = layout_and_paint(&mut w, Some(&theme));
let status = theme.extension::<StatusPalette>().unwrap();
let colors = status.colors(Brightness::Dark);
assert_eq!(rec.rrects[0].3, colors.info_container);
assert_eq!(rec.glyph_colors[0], colors.info);
assert_eq!(rec.strokes[0], with_alpha(colors.info, BADGE_BORDER_ALPHA));
}
#[test]
fn glyph_light_theme_resolves_info_from_status_palette() {
let theme = crate::baseline().with_brightness(Brightness::Light);
let mut w = build_widget(BadgeVariant::Info, false);
let rec = layout_and_paint(&mut w, Some(&theme));
let status = theme.extension::<StatusPalette>().unwrap();
let colors = status.colors(Brightness::Light);
assert_eq!(rec.rrects[0].3, colors.info_container);
assert_eq!(rec.glyph_colors[0], colors.info);
assert_eq!(rec.strokes[0], with_alpha(colors.info, BADGE_BORDER_ALPHA));
}
#[test]
fn glyph_theme_resolves_error_from_color_scheme_directly() {
let theme = crate::baseline();
let mut w = build_widget(BadgeVariant::Error, false);
let rec = layout_and_paint(&mut w, Some(&theme));
assert_eq!(rec.rrects[0].3, theme.scheme().error_container);
assert_eq!(rec.glyph_colors[0], theme.scheme().error);
}
#[test]
fn glyph_theme_resolves_radius_as_a_pill() {
let theme = crate::baseline();
let mut w = build_widget(BadgeVariant::Success, false);
let rec = layout_and_paint(&mut w, Some(&theme));
let (_, size, radius, _) = rec.rrects[0];
assert_eq!(radius, size.height / 2.0);
}
#[test]
fn all_variants_resolved_colors_are_pinned() {
let theme = crate::baseline();
let mut w = build_widget(BadgeVariant::Success, false);
let rec = layout_and_paint(&mut w, Some(&theme));
let status = theme.extension::<StatusPalette>().unwrap();
let sc = status.colors(Brightness::Dark);
assert_eq!(rec.rrects[0].3, sc.success_container);
assert_eq!(rec.glyph_colors[0], sc.success);
assert_eq!(rec.strokes.len(), 0, "Success should have no border");
let mut w = build_widget(BadgeVariant::Warning, false);
let rec = layout_and_paint(&mut w, Some(&theme));
assert_eq!(rec.rrects[0].3, sc.warning_container);
assert_eq!(rec.glyph_colors[0], sc.warning);
assert_eq!(
rec.strokes[0],
with_alpha(sc.warning, BADGE_BORDER_ALPHA),
"Warning should have alpha-blended border"
);
let mut w = build_widget(BadgeVariant::Error, false);
let rec = layout_and_paint(&mut w, Some(&theme));
assert_eq!(rec.rrects[0].3, theme.scheme().error_container);
assert_eq!(rec.glyph_colors[0], theme.scheme().error);
assert_eq!(rec.strokes.len(), 0, "Error should have no border");
let mut w = build_widget(BadgeVariant::Neutral, false);
let rec = layout_and_paint(&mut w, Some(&theme));
assert_eq!(rec.rrects[0].3, theme.scheme().surface_container_high);
assert_eq!(rec.glyph_colors[0], theme.scheme().on_surface_variant);
assert_eq!(
rec.strokes[0],
theme.scheme().outline,
"Neutral should have outline border"
);
let mut w = build_widget(BadgeVariant::Accent, false);
let rec = layout_and_paint(&mut w, Some(&theme));
assert_eq!(
rec.rrects[0].3,
with_alpha(theme.scheme().primary, BADGE_ACCENT_WASH_ALPHA)
);
assert_eq!(rec.glyph_colors[0], theme.scheme().primary);
assert_eq!(rec.strokes.len(), 0, "Accent should have no border");
}
#[test]
fn dot_widens_the_badge_and_paints_a_dot_colored_like_the_label() {
let mut plain = build_widget(BadgeVariant::Success, false);
let mut dotted = build_widget(BadgeVariant::Success, true);
let rec_plain = layout_and_paint(&mut plain, None);
let rec_dotted = layout_and_paint(&mut dotted, None);
assert!(rec_dotted.rrects[0].1.width > rec_plain.rrects[0].1.width);
assert_eq!(rec_dotted.rrects.len(), 2);
assert_eq!(rec_dotted.rrects[1].3, BADGE_SUCCESS_FG);
}
#[test]
fn label_is_rendered_lowercase() {
let w = build_widget(BadgeVariant::Neutral, false);
assert_eq!(w.label.content, "connected");
assert_eq!(w.label_text, "Connected");
}
#[test]
fn semantics_reports_status_role_and_original_casing() {
fn logic(_s: &mut ()) -> BadgeView {
badge("Connected", BadgeVariant::Success)
}
let mut root: frust_core::RenderRoot<(), BadgeView> = frust_core::RenderRoot::new();
let mut state = ();
root.rebuild(&mut logic, &mut state);
let mut tcx = TextContext::new();
root.layout_with_text(Size::new(200.0, 200.0), &mut tcx as &mut dyn Any);
let update = root.semantics();
let (_, node) = update
.nodes
.iter()
.find(|(_, n)| n.role() == Role::Status)
.expect("badge contributes a Role::Status node");
assert_eq!(node.label(), Some("Connected"));
}
#[cfg(feature = "bundled-fonts")]
#[test]
fn the_label_paints_in_its_role_face_under_the_glyph_theme() {
use super::typeface_probe::{Face, painted_faces};
let faces = painted_faces(
|_: &mut ()| badge("online", BadgeVariant::Success),
crate::baseline(),
Size::new(200.0, 60.0),
);
assert_eq!(faces, [Face::PlexMono]);
}
#[cfg(feature = "bundled-fonts")]
#[test]
fn the_label_follows_a_live_theme_family_swap() {
use super::typeface_probe::{Face, faces_across_a_live_swap};
let (before, after) = faces_across_a_live_swap(
|_: &mut ()| badge("online", BadgeVariant::Success),
Size::new(200.0, 60.0),
);
assert_eq!(before, [Face::PlexMono]);
assert_eq!(after, [Face::SpaceMono]);
}
}
#[cfg(all(test, feature = "bundled-fonts"))]
pub(crate) mod typeface_probe {
use std::any::Any;
use std::sync::{Once, OnceLock};
use frust::Theme;
use frust::authoring::scene::GlyphRun;
use frust::authoring::text::{FontFamily, GenericSlot, TextContext};
use frust::authoring::{PaintScene, View};
use frust_core::{FrameTime, RenderRoot};
use kurbo::{Point, Size};
use peniko::Color;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Face {
SpaceMono,
PlexMono,
Other,
}
fn bundled_faces() -> &'static [(&'static [u8], Face)] {
static FACES: OnceLock<Vec<(&'static [u8], Face)>> = OnceLock::new();
FACES.get_or_init(|| {
let mut tcx = TextContext::new();
crate::font_data()
.iter()
.map(|&bytes| {
let families = tcx
.register_fonts(bytes.to_vec())
.expect("a bundled Glyph face registers");
let face = match families.first().map(|family| family.name.as_str()) {
Some("Space Mono") => Face::SpaceMono,
Some("IBM Plex Mono") => Face::PlexMono,
other => panic!("unexpected bundled family {other:?}"),
};
(bytes, face)
})
.collect()
})
}
pub(crate) struct FaceRecorder {
pub(crate) faces: Vec<Face>,
}
impl FaceRecorder {
pub(crate) fn new() -> Self {
Self { faces: Vec::new() }
}
}
impl PaintScene for FaceRecorder {
fn fill_rect(&mut self, _o: Point, _s: Size, _c: Color) {}
fn draw_text(&mut self, _o: Point, _t: &str) {}
fn draw_glyph_run(&mut self, run: GlyphRun) {
let bytes = run.font.font().data.as_ref();
let face = bundled_faces()
.iter()
.find(|(face_bytes, _)| *face_bytes == bytes)
.map_or(Face::Other, |(_, face)| *face);
self.faces.push(face);
}
}
fn registered_context() -> TextContext {
bundled_faces();
TextContext::new()
}
pub(crate) fn text_context() -> TextContext {
assert_control();
registered_context()
}
pub(crate) fn inverted_glyph_theme() -> Theme {
let space = FontFamily::stack_with_generic(["Space Mono"], GenericSlot::Monospace);
let plex = FontFamily::stack_with_generic(["IBM Plex Mono"], GenericSlot::Monospace);
let mut theme = crate::baseline();
macro_rules! set_family {
($family:ident => $($role:ident),+ $(,)?) => {
$( theme.type_scale.$role.family = $family.clone(); )+
};
}
set_family!(plex =>
display_large, display_medium, display_small,
headline_large, headline_medium, headline_small,
display_large_emphasized, display_medium_emphasized, display_small_emphasized,
headline_large_emphasized, headline_medium_emphasized, headline_small_emphasized,
);
set_family!(space =>
title_large, title_medium, title_small,
body_large, body_medium, body_small,
label_large, label_medium, label_small,
title_large_emphasized, title_medium_emphasized, title_small_emphasized,
body_large_emphasized, body_medium_emphasized, body_small_emphasized,
label_large_emphasized, label_medium_emphasized, label_small_emphasized,
);
theme
}
const FRAME_NS: u64 = 16_000_000;
const SETTLE_NS: u64 = 5_000_000_000;
fn paint_pass<V: View<()>>(
root: &mut RenderRoot<(), V>,
window: Size,
tcx: &mut TextContext,
now: &mut u64,
) -> Vec<Face> {
root.layout_with_text(window, tcx as &mut dyn Any);
let deadline = *now + SETTLE_NS;
loop {
let mut recorder = FaceRecorder::new();
let outcome = root.paint(&mut recorder, FrameTime::from_nanos(*now));
if !(outcome.needs_frame || outcome.needs_layout) || *now >= deadline {
return recorder.faces;
}
if outcome.needs_layout {
root.layout_with_text(window, tcx as &mut dyn Any);
}
*now += FRAME_NS;
}
}
fn assert_control() {
static CONTROL: Once = Once::new();
CONTROL.call_once(|| {
let mut root: RenderRoot<(), _> = RenderRoot::new();
root.set_theme(Box::new(crate::baseline()));
root.rebuild(&mut |_: &mut ()| frust::text("Hello"), &mut ());
let faces = paint_pass(
&mut root,
Size::new(200.0, 60.0),
&mut registered_context(),
&mut 0,
);
assert!(
faces.iter().all(|face| *face == Face::Other),
"control: a plain text() that never asks for a Glyph family painted {faces:?} \
— on this host an un-opted run is indistinguishable from a bundled face, \
so the typeface assertions would prove nothing"
);
});
}
pub(crate) fn painted_faces<V: View<()>>(
mut logic: impl FnMut(&mut ()) -> V,
theme: Theme,
window: Size,
) -> Vec<Face> {
let mut tcx = text_context();
let mut root: RenderRoot<(), V> = RenderRoot::new();
root.set_theme(Box::new(theme));
root.rebuild(&mut logic, &mut ());
paint_pass(&mut root, window, &mut tcx, &mut 0)
}
pub(crate) fn faces_across_a_live_swap<V: View<()>>(
mut logic: impl FnMut(&mut ()) -> V,
window: Size,
) -> (Vec<Face>, Vec<Face>) {
let mut tcx = text_context();
let mut root: RenderRoot<(), V> = RenderRoot::new();
root.set_theme(Box::new(crate::baseline()));
root.rebuild(&mut logic, &mut ());
let mut now = 0;
let before = paint_pass(&mut root, window, &mut tcx, &mut now);
root.set_theme(Box::new(inverted_glyph_theme()));
let after = paint_pass(&mut root, window, &mut tcx, &mut now);
(before, after)
}
}