use crate::stroke::{arc_band, ArcGeometry, Stroke};
use crate::typography::{
estimate_text_measurement, DrawTextMeasurer, TextAlign, TextMeasurement, TextStyle,
TextVerticalAlign,
};
use crate::{Brush, Color, ColorFilter, ImageBitmap, ImageSampling};
use std::ops::AddAssign;
use std::rc::Rc;
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct Point {
pub x: f32,
pub y: f32,
}
impl Point {
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
pub const ZERO: Point = Point { x: 0.0, y: 0.0 };
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct Size {
pub width: f32,
pub height: f32,
}
impl Size {
pub const fn new(width: f32, height: f32) -> Self {
Self { width, height }
}
pub const ZERO: Size = Size {
width: 0.0,
height: 0.0,
};
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl Rect {
pub fn from_origin_size(origin: Point, size: Size) -> Self {
Self {
x: origin.x,
y: origin.y,
width: size.width,
height: size.height,
}
}
pub fn from_size(size: Size) -> Self {
Self {
x: 0.0,
y: 0.0,
width: size.width,
height: size.height,
}
}
pub fn translate(&self, dx: f32, dy: f32) -> Self {
Self {
x: self.x + dx,
y: self.y + dy,
width: self.width,
height: self.height,
}
}
pub fn contains(&self, x: f32, y: f32) -> bool {
x >= self.x && y >= self.y && x <= self.x + self.width && y <= self.y + self.height
}
pub fn intersect(&self, other: Rect) -> Option<Rect> {
let left = self.x.max(other.x);
let top = self.y.max(other.y);
let right = (self.x + self.width).min(other.x + other.width);
let bottom = (self.y + self.height).min(other.y + other.height);
let width = right - left;
let height = bottom - top;
if width <= 0.0 || height <= 0.0 {
None
} else {
Some(Rect {
x: left,
y: top,
width,
height,
})
}
}
pub fn union(&self, other: Rect) -> Rect {
let left = self.x.min(other.x);
let top = self.y.min(other.y);
let right = (self.x + self.width).max(other.x + other.width);
let bottom = (self.y + self.height).max(other.y + other.height);
Rect {
x: left,
y: top,
width: (right - left).max(0.0),
height: (bottom - top).max(0.0),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct EdgeInsets {
pub left: f32,
pub top: f32,
pub right: f32,
pub bottom: f32,
}
impl EdgeInsets {
pub fn uniform(all: f32) -> Self {
Self {
left: all,
top: all,
right: all,
bottom: all,
}
}
pub fn horizontal(horizontal: f32) -> Self {
Self {
left: horizontal,
right: horizontal,
..Self::default()
}
}
pub fn vertical(vertical: f32) -> Self {
Self {
top: vertical,
bottom: vertical,
..Self::default()
}
}
pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
Self {
left: horizontal,
right: horizontal,
top: vertical,
bottom: vertical,
}
}
pub fn from_components(left: f32, top: f32, right: f32, bottom: f32) -> Self {
Self {
left,
top,
right,
bottom,
}
}
pub fn is_zero(&self) -> bool {
self.left == 0.0 && self.top == 0.0 && self.right == 0.0 && self.bottom == 0.0
}
pub fn horizontal_sum(&self) -> f32 {
self.left + self.right
}
pub fn vertical_sum(&self) -> f32 {
self.top + self.bottom
}
}
impl AddAssign for EdgeInsets {
fn add_assign(&mut self, rhs: Self) {
self.left += rhs.left;
self.top += rhs.top;
self.right += rhs.right;
self.bottom += rhs.bottom;
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct CornerRadii {
pub top_left: f32,
pub top_right: f32,
pub bottom_right: f32,
pub bottom_left: f32,
}
impl CornerRadii {
pub fn uniform(radius: f32) -> Self {
Self {
top_left: radius,
top_right: radius,
bottom_right: radius,
bottom_left: radius,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RoundedCornerShape {
radii: CornerRadii,
}
impl RoundedCornerShape {
pub fn new(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self {
Self {
radii: CornerRadii {
top_left,
top_right,
bottom_right,
bottom_left,
},
}
}
pub fn uniform(radius: f32) -> Self {
Self {
radii: CornerRadii::uniform(radius),
}
}
pub fn with_radii(radii: CornerRadii) -> Self {
Self { radii }
}
pub fn resolve(&self, width: f32, height: f32) -> CornerRadii {
let mut resolved = self.radii;
let max_width = (width / 2.0).max(0.0);
let max_height = (height / 2.0).max(0.0);
resolved.top_left = resolved.top_left.clamp(0.0, max_width).min(max_height);
resolved.top_right = resolved.top_right.clamp(0.0, max_width).min(max_height);
resolved.bottom_right = resolved.bottom_right.clamp(0.0, max_width).min(max_height);
resolved.bottom_left = resolved.bottom_left.clamp(0.0, max_width).min(max_height);
resolved
}
pub fn radii(&self) -> CornerRadii {
self.radii
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TransformOrigin {
pub pivot_fraction_x: f32,
pub pivot_fraction_y: f32,
}
impl TransformOrigin {
pub const fn new(pivot_fraction_x: f32, pivot_fraction_y: f32) -> Self {
Self {
pivot_fraction_x,
pivot_fraction_y,
}
}
pub const CENTER: TransformOrigin = TransformOrigin::new(0.5, 0.5);
}
impl Default for TransformOrigin {
fn default() -> Self {
Self::CENTER
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum LayerShape {
#[default]
Rectangle,
Rounded(RoundedCornerShape),
}
#[derive(Clone, Debug, PartialEq)]
pub struct GraphicsLayer {
pub alpha: f32,
pub scale: f32,
pub scale_x: f32,
pub scale_y: f32,
pub rotation_x: f32,
pub rotation_y: f32,
pub rotation_z: f32,
pub camera_distance: f32,
pub transform_origin: TransformOrigin,
pub translation_x: f32,
pub translation_y: f32,
pub shadow_elevation: f32,
pub ambient_shadow_color: Color,
pub spot_shadow_color: Color,
pub shape: LayerShape,
pub clip: bool,
pub compositing_strategy: CompositingStrategy,
pub blend_mode: BlendMode,
pub color_filter: Option<ColorFilter>,
pub render_effect: Option<crate::render_effect::RenderEffect>,
pub backdrop_effect: Option<crate::render_effect::RenderEffect>,
}
impl Default for GraphicsLayer {
fn default() -> Self {
Self {
alpha: 1.0,
scale: 1.0,
scale_x: 1.0,
scale_y: 1.0,
rotation_x: 0.0,
rotation_y: 0.0,
rotation_z: 0.0,
camera_distance: 8.0,
transform_origin: TransformOrigin::CENTER,
translation_x: 0.0,
translation_y: 0.0,
shadow_elevation: 0.0,
ambient_shadow_color: Color::BLACK,
spot_shadow_color: Color::BLACK,
shape: LayerShape::Rectangle,
clip: false,
compositing_strategy: CompositingStrategy::Auto,
blend_mode: BlendMode::SrcOver,
color_filter: None,
render_effect: None,
backdrop_effect: None,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum BlendMode {
Clear,
Src,
Dst,
#[default]
SrcOver,
DstOver,
SrcIn,
DstIn,
SrcOut,
DstOut,
SrcAtop,
DstAtop,
Xor,
Plus,
Modulate,
Screen,
Overlay,
Darken,
Lighten,
ColorDodge,
ColorBurn,
HardLight,
SoftLight,
Difference,
Exclusion,
Multiply,
Hue,
Saturation,
Color,
Luminosity,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum CompositingStrategy {
#[default]
Auto,
Offscreen,
ModulateAlpha,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DrawPrimitive {
Content,
Blend {
primitive: Box<DrawPrimitive>,
blend_mode: BlendMode,
},
Rect {
rect: Rect,
brush: Brush,
stroke: Option<Stroke>,
},
RoundRect {
rect: Rect,
brush: Brush,
radii: CornerRadii,
stroke: Option<Stroke>,
},
Arc {
rect: Rect,
brush: Brush,
center: Point,
radius: f32,
start_angle: f32,
sweep_angle: f32,
stroke: Option<Stroke>,
inner_radius: f32,
},
Image {
rect: Rect,
image: ImageBitmap,
alpha: f32,
color_filter: Option<ColorFilter>,
sampling: ImageSampling,
src_rect: Option<Rect>,
},
Text(Box<TextPrimitive>),
Shadow(ShadowPrimitive),
}
#[derive(Clone, Debug, PartialEq)]
pub struct TextPrimitive {
pub rect: Rect,
pub text: std::rc::Rc<str>,
pub style: TextStyle,
pub color: Color,
}
fn shared_text_str(text: &str) -> Rc<str> {
use std::cell::RefCell;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
const POOL_CAPACITY: usize = 256;
thread_local! {
static POOL: RefCell<HashMap<u64, Rc<str>>> = RefCell::new(HashMap::new());
}
let mut hasher = crate::FxHasher::default();
text.hash(&mut hasher);
let key = hasher.finish();
POOL.with(|pool| {
let mut pool = pool.borrow_mut();
if let Some(shared) = pool.get(&key) {
if &**shared == text {
return Rc::clone(shared);
}
}
let shared: Rc<str> = Rc::from(text);
if pool.len() >= POOL_CAPACITY {
pool.clear();
}
pool.insert(key, Rc::clone(&shared));
shared
})
}
#[derive(Clone, Debug, PartialEq)]
pub enum ShadowPrimitive {
Drop {
shape: Box<DrawPrimitive>,
cutout: Option<Box<DrawPrimitive>>,
blur_radius: f32,
blend_mode: BlendMode,
},
Inner {
fill: Box<DrawPrimitive>,
cutout: Box<DrawPrimitive>,
blur_radius: f32,
blend_mode: BlendMode,
clip_rect: Rect,
},
}
pub trait DrawScope {
fn size(&self) -> Size;
fn draw_content(&mut self);
fn draw_rect(&mut self, brush: Brush);
fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode);
fn draw_rect_at(&mut self, rect: Rect, brush: Brush);
fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode);
fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii);
fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode);
fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii);
fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32);
fn draw_circle_blend(
&mut self,
brush: Brush,
center: Point,
radius: f32,
blend_mode: BlendMode,
);
fn draw_rect_stroked(&mut self, brush: Brush, stroke: Stroke);
fn draw_rect_stroked_blend(&mut self, brush: Brush, stroke: Stroke, blend_mode: BlendMode);
fn draw_rect_at_stroked(&mut self, rect: Rect, brush: Brush, stroke: Stroke);
fn draw_rect_at_stroked_blend(
&mut self,
rect: Rect,
brush: Brush,
stroke: Stroke,
blend_mode: BlendMode,
);
fn draw_round_rect_stroked(&mut self, brush: Brush, radii: CornerRadii, stroke: Stroke);
fn draw_round_rect_stroked_blend(
&mut self,
brush: Brush,
radii: CornerRadii,
stroke: Stroke,
blend_mode: BlendMode,
);
fn draw_round_rect_at_stroked(
&mut self,
rect: Rect,
brush: Brush,
radii: CornerRadii,
stroke: Stroke,
);
#[allow(clippy::too_many_arguments)]
fn draw_round_rect_at_stroked_blend(
&mut self,
rect: Rect,
brush: Brush,
radii: CornerRadii,
stroke: Stroke,
blend_mode: BlendMode,
);
fn draw_circle_stroked(&mut self, brush: Brush, center: Point, radius: f32, stroke: Stroke);
fn draw_circle_stroked_blend(
&mut self,
brush: Brush,
center: Point,
radius: f32,
stroke: Stroke,
blend_mode: BlendMode,
);
#[allow(clippy::too_many_arguments)]
fn draw_arc(
&mut self,
brush: Brush,
center: Point,
radius: f32,
start_angle: f32,
sweep_angle: f32,
stroke: Stroke,
);
#[allow(clippy::too_many_arguments)]
fn draw_arc_blend(
&mut self,
brush: Brush,
center: Point,
radius: f32,
start_angle: f32,
sweep_angle: f32,
stroke: Stroke,
blend_mode: BlendMode,
);
#[allow(clippy::too_many_arguments)]
fn draw_annular_sector(
&mut self,
brush: Brush,
center: Point,
inner_radius: f32,
outer_radius: f32,
start_angle: f32,
sweep_angle: f32,
);
#[allow(clippy::too_many_arguments)]
fn draw_annular_sector_blend(
&mut self,
brush: Brush,
center: Point,
inner_radius: f32,
outer_radius: f32,
start_angle: f32,
sweep_angle: f32,
blend_mode: BlendMode,
);
fn draw_image(&mut self, image: ImageBitmap);
fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode);
fn draw_image_at(
&mut self,
rect: Rect,
image: ImageBitmap,
alpha: f32,
color_filter: Option<ColorFilter>,
);
fn draw_image_at_sampled(
&mut self,
rect: Rect,
image: ImageBitmap,
alpha: f32,
color_filter: Option<ColorFilter>,
sampling: ImageSampling,
);
fn draw_image_at_blend(
&mut self,
rect: Rect,
image: ImageBitmap,
alpha: f32,
color_filter: Option<ColorFilter>,
blend_mode: BlendMode,
);
fn draw_image_src(
&mut self,
image: ImageBitmap,
src_rect: Rect,
dst_rect: Rect,
alpha: f32,
color_filter: Option<ColorFilter>,
);
fn draw_image_src_sampled(
&mut self,
image: ImageBitmap,
src_rect: Rect,
dst_rect: Rect,
alpha: f32,
color_filter: Option<ColorFilter>,
sampling: ImageSampling,
);
fn draw_image_src_blend(
&mut self,
image: ImageBitmap,
src_rect: Rect,
dst_rect: Rect,
alpha: f32,
color_filter: Option<ColorFilter>,
blend_mode: BlendMode,
);
fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush);
fn draw_svg_path(&mut self, d: &str, brush: Brush) {
if let Ok(path) = crate::VectorPath::parse(d) {
self.draw_vector_path(&path, brush);
}
}
fn measure_text(&self, text: &str, style: &TextStyle) -> TextMeasurement;
fn draw_text(&mut self, brush: Brush, text: &str, style: &TextStyle) {
self.draw_text_at(Rect::from_size(self.size()), brush, text, style);
}
fn draw_text_at(&mut self, rect: Rect, brush: Brush, text: &str, style: &TextStyle);
fn draw_text_from(&mut self, top_left: Point, brush: Brush, text: &str, style: &TextStyle) {
if text.is_empty() {
return;
}
let measurement = self.measure_text(text, style);
self.draw_text_at(
Rect::from_origin_size(top_left, measurement.size),
brush,
text,
&TextStyle {
align: TextAlign::Left,
vertical_align: TextVerticalAlign::Top,
..style.clone()
},
);
}
fn into_primitives(self) -> Vec<DrawPrimitive>;
}
pub fn align_text_block(rect: Rect, measurement: TextMeasurement, style: &TextStyle) -> Point {
let x = match style.align {
TextAlign::Left => rect.x,
TextAlign::Center => rect.x + (rect.width - measurement.size.width) * 0.5,
TextAlign::Right => rect.x + rect.width - measurement.size.width,
};
let y = match style.vertical_align {
TextVerticalAlign::Top => rect.y,
TextVerticalAlign::Center => rect.y + (rect.height - measurement.size.height) * 0.5,
TextVerticalAlign::Bottom => rect.y + rect.height - measurement.size.height,
TextVerticalAlign::Baseline => rect.y - measurement.first_baseline,
};
Point::new(x, y)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub(crate) enum RecordKind {
SolidRect,
SolidRoundRect,
SolidArc,
Other,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct TapeRef(u32);
impl TapeRef {
const KIND_SHIFT: u32 = 30;
const INDEX_MASK: u32 = (1 << Self::KIND_SHIFT) - 1;
pub(crate) fn new(kind: RecordKind, index: usize) -> Self {
debug_assert!(index < (1usize << Self::KIND_SHIFT));
Self(((kind as u32) << Self::KIND_SHIFT) | index as u32)
}
pub(crate) fn kind(self) -> RecordKind {
match self.0 >> Self::KIND_SHIFT {
0 => RecordKind::SolidRect,
1 => RecordKind::SolidRoundRect,
2 => RecordKind::SolidArc,
_ => RecordKind::Other,
}
}
pub(crate) fn index(self) -> usize {
(self.0 & Self::INDEX_MASK) as usize
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SolidRectRecord {
pub rect: Rect,
pub color: Color,
pub stroke: Option<Stroke>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SolidRoundRectRecord {
pub rect: Rect,
pub radii: CornerRadii,
pub color: Color,
pub stroke: Option<Stroke>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SolidArcRecord {
pub center: Point,
pub radius: f32,
pub start_angle: f32,
pub sweep_angle: f32,
pub inner_radius: f32,
pub color: Color,
pub stroke: Option<Stroke>,
}
#[derive(Clone, Debug, Default)]
pub struct CommandRecording {
pub(crate) tape: Vec<TapeRef>,
pub(crate) rects: Vec<SolidRectRecord>,
pub(crate) round_rects: Vec<SolidRoundRectRecord>,
pub(crate) arcs: Vec<SolidArcRecord>,
pub(crate) others: Vec<DrawPrimitive>,
}
impl CommandRecording {
pub fn len(&self) -> usize {
self.tape.len()
}
pub fn materialize_range(
&self,
tape_start: usize,
tape_end: usize,
) -> Option<Vec<DrawPrimitive>> {
if tape_start > tape_end || tape_end > self.tape.len() {
return None;
}
let mut out = Vec::with_capacity(tape_end - tape_start);
for entry in &self.tape[tape_start..tape_end] {
match entry.kind() {
RecordKind::SolidRect => {
let record = self.rects.get(entry.index())?;
out.push(DrawPrimitive::Rect {
rect: record.rect,
brush: Brush::Solid(record.color),
stroke: record.stroke,
});
}
RecordKind::SolidRoundRect => {
let record = self.round_rects.get(entry.index())?;
out.push(DrawPrimitive::RoundRect {
rect: record.rect,
brush: Brush::Solid(record.color),
radii: record.radii,
stroke: record.stroke,
});
}
RecordKind::SolidArc => {
let record = self.arcs.get(entry.index())?;
if let Some(primitive) = materialize_solid_arc(record) {
out.push(primitive);
}
}
RecordKind::Other => {
out.push(self.others.get(entry.index())?.clone());
}
}
}
Some(out)
}
pub fn is_empty(&self) -> bool {
self.tape.is_empty()
}
#[doc(hidden)]
pub fn tape_ptr(&self) -> *const u8 {
self.tape.as_ptr() as *const u8
}
fn clear(&mut self) {
self.tape.clear();
self.rects.clear();
self.round_rects.clear();
self.arcs.clear();
self.others.clear();
}
}
pub struct FinishedRecording {
pub primitives: Vec<DrawPrimitive>,
pub content_markers: u32,
pub recording: CommandRecording,
pub dropped: Vec<u32>,
}
#[derive(Default)]
pub struct DrawScopeDefault {
size: Size,
rec: CommandRecording,
out: Vec<DrawPrimitive>,
content_markers: u32,
text_measurer: Option<Rc<dyn DrawTextMeasurer>>,
}
const RECORDED_PRIMITIVE_COUNTS_LIMIT: usize = 64;
thread_local! {
static RECORDED_PRIMITIVE_COUNTS: std::cell::RefCell<std::collections::HashMap<(u32, u32), usize>> =
std::cell::RefCell::new(std::collections::HashMap::new());
}
fn recorded_primitive_capacity(size: Size) -> usize {
RECORDED_PRIMITIVE_COUNTS.with(|counts| {
counts
.borrow()
.get(&(size.width.to_bits(), size.height.to_bits()))
.copied()
.unwrap_or(0)
})
}
fn note_recorded_primitive_count(size: Size, count: usize) {
RECORDED_PRIMITIVE_COUNTS.with(|counts| {
let mut counts = counts.borrow_mut();
if counts.len() >= RECORDED_PRIMITIVE_COUNTS_LIMIT {
counts.clear();
}
counts.insert((size.width.to_bits(), size.height.to_bits()), count);
});
}
impl DrawScopeDefault {
pub fn new(size: Size) -> Self {
Self::with_recording(size, None, CommandRecording::default(), Vec::new())
}
pub fn with_text_measurer(size: Size, text_measurer: Rc<dyn DrawTextMeasurer>) -> Self {
Self::with_recording(
size,
Some(text_measurer),
CommandRecording::default(),
Vec::new(),
)
}
pub fn with_text_measurer_reusing(
size: Size,
text_measurer: Rc<dyn DrawTextMeasurer>,
storage: Vec<DrawPrimitive>,
) -> Self {
Self::with_recording(
size,
Some(text_measurer),
CommandRecording::default(),
storage,
)
}
pub fn with_recording(
size: Size,
text_measurer: Option<Rc<dyn DrawTextMeasurer>>,
mut recording: CommandRecording,
out: Vec<DrawPrimitive>,
) -> Self {
recording.clear();
recording.tape.reserve(recorded_primitive_capacity(size));
Self {
size,
rec: recording,
out,
content_markers: 0,
text_measurer,
}
}
pub fn content_marker_count(&self) -> u32 {
self.content_markers
}
pub fn recorded(&self) -> &CommandRecording {
&self.rec
}
pub fn push_recorded(&mut self, primitives: Vec<DrawPrimitive>) {
self.content_markers += primitives
.iter()
.filter(|primitive| matches!(primitive, DrawPrimitive::Content))
.count() as u32;
let base = self.rec.others.len();
self.rec
.tape
.extend((0..primitives.len()).map(|i| TapeRef::new(RecordKind::Other, base + i)));
self.rec.others.extend(primitives);
}
pub fn finish(mut self) -> FinishedRecording {
if std::env::var_os("CRANPOSE_RECORD_MIX_DIAG").is_some() && self.rec.tape.len() > 400 {
eprintln!(
"[record-mix] tape={} rects={} round_rects={} arcs={} others={}",
self.rec.tape.len(),
self.rec.rects.len(),
self.rec.round_rects.len(),
self.rec.arcs.len(),
self.rec.others.len(),
);
}
let mut out = std::mem::take(&mut self.out);
out.clear();
out.reserve(self.rec.tape.len());
let mut dropped: Vec<u32> = Vec::new();
{
let mut others = self.rec.others.drain(..);
for (tape_index, entry) in self.rec.tape.iter().enumerate() {
match entry.kind() {
RecordKind::SolidRect => {
let record = &self.rec.rects[entry.index()];
out.push(DrawPrimitive::Rect {
rect: record.rect,
brush: Brush::Solid(record.color),
stroke: record.stroke,
});
}
RecordKind::SolidRoundRect => {
let record = &self.rec.round_rects[entry.index()];
out.push(DrawPrimitive::RoundRect {
rect: record.rect,
brush: Brush::Solid(record.color),
radii: record.radii,
stroke: record.stroke,
});
}
RecordKind::SolidArc => {
let record = &self.rec.arcs[entry.index()];
if let Some(primitive) = materialize_solid_arc(record) {
out.push(primitive);
} else {
dropped.push(tape_index as u32);
}
}
RecordKind::Other => {
out.push(others.next().expect("tape/others in sync"));
}
}
}
}
self.rec.clear();
note_recorded_primitive_count(self.size, out.len());
FinishedRecording {
primitives: out,
content_markers: self.content_markers,
recording: self.rec,
dropped,
}
}
pub fn finish_replay(
mut self,
center: Point,
outcome: crate::record_replay::ReplayOutcome,
bypass: &mut dyn FnMut(u32) -> bool,
) -> (
FinishedRecording,
Option<crate::record_replay::CommandReplayFrame>,
) {
use crate::record_replay::{CommandReplayFrame, FrameSpan, ReplayOutcome, ReplaySpan};
let ReplayOutcome::Spans(replay_spans) = outcome else {
return (self.finish(), None);
};
let tape_len = self.rec.tape.len();
let mut out = std::mem::take(&mut self.out);
out.clear();
let mut dropped: Vec<u32> = Vec::new();
let mut spans: Vec<FrameSpan> = Vec::with_capacity(replay_spans.len());
let mut any_retained = false;
{
let mut others = self.rec.others.drain(..);
let tape = &self.rec.tape;
let rects = &self.rec.rects;
let round_rects = &self.rec.round_rects;
let arcs = &self.rec.arcs;
macro_rules! materialize_range {
($start:expr, $end:expr) => {{
let prim_start = out.len() as u32;
for tape_index in $start..$end {
let entry = tape[tape_index];
match entry.kind() {
RecordKind::SolidRect => {
let record = &rects[entry.index()];
out.push(DrawPrimitive::Rect {
rect: record.rect,
brush: Brush::Solid(record.color),
stroke: record.stroke,
});
}
RecordKind::SolidRoundRect => {
let record = &round_rects[entry.index()];
out.push(DrawPrimitive::RoundRect {
rect: record.rect,
brush: Brush::Solid(record.color),
radii: record.radii,
stroke: record.stroke,
});
}
RecordKind::SolidArc => {
let record = &arcs[entry.index()];
if let Some(primitive) = materialize_solid_arc(record) {
out.push(primitive);
} else {
dropped.push(tape_index as u32);
}
}
RecordKind::Other => {
out.push(others.next().expect("tape/others in sync"));
}
}
}
(prim_start, out.len() as u32)
}};
}
for span in replay_spans {
match span {
ReplaySpan::Dynamic {
tape_start,
tape_end,
} => {
let range = materialize_range!(tape_start, tape_end);
if range.1 > range.0 {
spans.push(FrameSpan::Dynamic { range });
}
}
ReplaySpan::Retained {
slot,
capture,
slot_offset,
tape_start,
tape_end,
transform,
recolors,
bounds,
} => {
let compact = tape[tape_start..tape_end]
.iter()
.all(|entry| entry.kind() != RecordKind::Other);
if compact && !capture && bypass(slot) {
any_retained = true;
let position = out.len() as u32;
spans.push(FrameSpan::Retained {
slot,
capture: false,
slot_offset: slot_offset as u32,
range: (position, position),
tape_range: (tape_start as u32, tape_end as u32),
transform,
recolors,
bounds,
});
continue;
}
let drops_before = dropped.len();
let range = materialize_range!(tape_start, tape_end);
if compact && dropped.len() == drops_before {
any_retained = true;
spans.push(FrameSpan::Retained {
slot,
capture,
slot_offset: slot_offset as u32,
range,
tape_range: (tape_start as u32, tape_end as u32),
transform,
recolors,
bounds,
});
} else if range.1 > range.0 {
spans.push(FrameSpan::Dynamic { range });
}
}
}
}
}
note_recorded_primitive_count(self.size, tape_len);
let frame = any_retained.then_some(CommandReplayFrame {
center,
spans,
fallback: None,
});
(
FinishedRecording {
primitives: out,
content_markers: self.content_markers,
recording: self.rec,
dropped,
},
frame,
)
}
fn push_other(&mut self, primitive: DrawPrimitive) {
let idx = self.rec.others.len();
self.rec.tape.push(TapeRef::new(RecordKind::Other, idx));
self.rec.others.push(primitive);
}
fn push_blended_primitive(&mut self, primitive: DrawPrimitive, blend_mode: BlendMode) {
if blend_mode != BlendMode::SrcOver {
self.push_other(DrawPrimitive::Blend {
primitive: Box::new(primitive),
blend_mode,
});
return;
}
match primitive {
DrawPrimitive::Rect {
rect,
brush: Brush::Solid(color),
stroke,
} => {
let idx = self.rec.rects.len();
self.rec.tape.push(TapeRef::new(RecordKind::SolidRect, idx));
self.rec.rects.push(SolidRectRecord {
rect,
color,
stroke,
});
}
DrawPrimitive::RoundRect {
rect,
brush: Brush::Solid(color),
radii,
stroke,
} => {
let idx = self.rec.round_rects.len();
self.rec
.tape
.push(TapeRef::new(RecordKind::SolidRoundRect, idx));
self.rec.round_rects.push(SolidRoundRectRecord {
rect,
radii,
color,
stroke,
});
}
other => self.push_other(other),
}
}
#[allow(clippy::too_many_arguments)]
fn push_arc(
&mut self,
brush: Brush,
center: Point,
radius: f32,
start_angle: f32,
sweep_angle: f32,
stroke: Option<Stroke>,
inner_radius: f32,
blend_mode: BlendMode,
) {
if blend_mode == BlendMode::SrcOver {
if let Brush::Solid(color) = brush {
let idx = self.rec.arcs.len();
self.rec.tape.push(TapeRef::new(RecordKind::SolidArc, idx));
self.rec.arcs.push(SolidArcRecord {
center,
radius,
start_angle,
sweep_angle,
inner_radius,
color,
stroke,
});
return;
}
}
let (band_inner, band_outer, cap) = arc_band(radius, inner_radius, stroke);
let geometry = ArcGeometry::new(
center,
band_inner,
band_outer,
start_angle,
sweep_angle,
cap,
);
if geometry.is_degenerate() {
return;
}
self.push_blended_primitive(
DrawPrimitive::Arc {
rect: geometry.bounds(),
brush,
center,
radius,
start_angle,
sweep_angle,
stroke,
inner_radius,
},
blend_mode,
);
}
}
fn materialize_solid_arc(record: &SolidArcRecord) -> Option<DrawPrimitive> {
let (band_inner, band_outer, cap) = arc_band(record.radius, record.inner_radius, record.stroke);
let geometry = ArcGeometry::new(
record.center,
band_inner,
band_outer,
record.start_angle,
record.sweep_angle,
cap,
);
if geometry.is_degenerate() {
return None;
}
Some(DrawPrimitive::Arc {
rect: geometry.bounds(),
brush: Brush::Solid(record.color),
center: record.center,
radius: record.radius,
start_angle: record.start_angle,
sweep_angle: record.sweep_angle,
stroke: record.stroke,
inner_radius: record.inner_radius,
})
}
impl DrawScope for DrawScopeDefault {
fn size(&self) -> Size {
self.size
}
fn draw_content(&mut self) {
self.content_markers += 1;
self.push_other(DrawPrimitive::Content);
}
fn draw_rect(&mut self, brush: Brush) {
self.draw_rect_blend(brush, BlendMode::SrcOver);
}
fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode) {
self.push_blended_primitive(
DrawPrimitive::Rect {
rect: Rect::from_size(self.size),
brush,
stroke: None,
},
blend_mode,
);
}
fn draw_rect_at(&mut self, rect: Rect, brush: Brush) {
self.draw_rect_at_blend(rect, brush, BlendMode::SrcOver);
}
fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode) {
self.push_blended_primitive(
DrawPrimitive::Rect {
rect,
brush,
stroke: None,
},
blend_mode,
);
}
fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii) {
self.draw_round_rect_blend(brush, radii, BlendMode::SrcOver);
}
fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode) {
self.push_blended_primitive(
DrawPrimitive::RoundRect {
rect: Rect::from_size(self.size),
brush,
radii,
stroke: None,
},
blend_mode,
);
}
fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii) {
self.push_blended_primitive(
DrawPrimitive::RoundRect {
rect,
brush,
radii,
stroke: None,
},
BlendMode::SrcOver,
);
}
fn draw_rect_stroked(&mut self, brush: Brush, stroke: Stroke) {
self.draw_rect_stroked_blend(brush, stroke, BlendMode::SrcOver);
}
fn draw_rect_stroked_blend(&mut self, brush: Brush, stroke: Stroke, blend_mode: BlendMode) {
self.draw_rect_at_stroked_blend(Rect::from_size(self.size), brush, stroke, blend_mode);
}
fn draw_rect_at_stroked(&mut self, rect: Rect, brush: Brush, stroke: Stroke) {
self.draw_rect_at_stroked_blend(rect, brush, stroke, BlendMode::SrcOver);
}
fn draw_rect_at_stroked_blend(
&mut self,
rect: Rect,
brush: Brush,
stroke: Stroke,
blend_mode: BlendMode,
) {
if !stroke.is_visible() {
return;
}
self.push_blended_primitive(
DrawPrimitive::Rect {
rect,
brush,
stroke: Some(stroke),
},
blend_mode,
);
}
fn draw_round_rect_stroked(&mut self, brush: Brush, radii: CornerRadii, stroke: Stroke) {
self.draw_round_rect_stroked_blend(brush, radii, stroke, BlendMode::SrcOver);
}
fn draw_round_rect_stroked_blend(
&mut self,
brush: Brush,
radii: CornerRadii,
stroke: Stroke,
blend_mode: BlendMode,
) {
self.draw_round_rect_at_stroked_blend(
Rect::from_size(self.size),
brush,
radii,
stroke,
blend_mode,
);
}
fn draw_round_rect_at_stroked(
&mut self,
rect: Rect,
brush: Brush,
radii: CornerRadii,
stroke: Stroke,
) {
self.draw_round_rect_at_stroked_blend(rect, brush, radii, stroke, BlendMode::SrcOver);
}
fn draw_round_rect_at_stroked_blend(
&mut self,
rect: Rect,
brush: Brush,
radii: CornerRadii,
stroke: Stroke,
blend_mode: BlendMode,
) {
if !stroke.is_visible() {
return;
}
self.push_blended_primitive(
DrawPrimitive::RoundRect {
rect,
brush,
radii,
stroke: Some(stroke),
},
blend_mode,
);
}
fn draw_circle_stroked(&mut self, brush: Brush, center: Point, radius: f32, stroke: Stroke) {
self.draw_circle_stroked_blend(brush, center, radius, stroke, BlendMode::SrcOver);
}
fn draw_circle_stroked_blend(
&mut self,
brush: Brush,
center: Point,
radius: f32,
stroke: Stroke,
blend_mode: BlendMode,
) {
if !stroke.is_visible() || !radius.is_finite() {
return;
}
let radius = radius.max(0.0);
let diameter = radius * 2.0;
self.draw_round_rect_at_stroked_blend(
Rect {
x: center.x - radius,
y: center.y - radius,
width: diameter,
height: diameter,
},
brush,
CornerRadii::uniform(radius),
stroke,
blend_mode,
);
}
fn draw_arc(
&mut self,
brush: Brush,
center: Point,
radius: f32,
start_angle: f32,
sweep_angle: f32,
stroke: Stroke,
) {
self.draw_arc_blend(
brush,
center,
radius,
start_angle,
sweep_angle,
stroke,
BlendMode::SrcOver,
);
}
fn draw_arc_blend(
&mut self,
brush: Brush,
center: Point,
radius: f32,
start_angle: f32,
sweep_angle: f32,
stroke: Stroke,
blend_mode: BlendMode,
) {
if !stroke.is_visible() {
return;
}
self.push_arc(
brush,
center,
radius,
start_angle,
sweep_angle,
Some(stroke),
0.0,
blend_mode,
);
}
fn draw_annular_sector(
&mut self,
brush: Brush,
center: Point,
inner_radius: f32,
outer_radius: f32,
start_angle: f32,
sweep_angle: f32,
) {
self.draw_annular_sector_blend(
brush,
center,
inner_radius,
outer_radius,
start_angle,
sweep_angle,
BlendMode::SrcOver,
);
}
fn draw_annular_sector_blend(
&mut self,
brush: Brush,
center: Point,
inner_radius: f32,
outer_radius: f32,
start_angle: f32,
sweep_angle: f32,
blend_mode: BlendMode,
) {
self.push_arc(
brush,
center,
outer_radius,
start_angle,
sweep_angle,
None,
inner_radius,
blend_mode,
);
}
fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32) {
self.draw_circle_blend(brush, center, radius, BlendMode::SrcOver);
}
fn draw_circle_blend(
&mut self,
brush: Brush,
center: Point,
radius: f32,
blend_mode: BlendMode,
) {
let radius = radius.max(0.0);
let diameter = radius * 2.0;
self.push_blended_primitive(
DrawPrimitive::RoundRect {
rect: Rect {
x: center.x - radius,
y: center.y - radius,
width: diameter,
height: diameter,
},
brush,
radii: CornerRadii::uniform(radius),
stroke: None,
},
blend_mode,
);
}
fn draw_image(&mut self, image: ImageBitmap) {
self.draw_image_blend(image, BlendMode::SrcOver);
}
fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode) {
self.push_blended_primitive(
DrawPrimitive::Image {
rect: Rect::from_size(self.size),
image,
alpha: 1.0,
color_filter: None,
sampling: ImageSampling::Nearest,
src_rect: None,
},
blend_mode,
);
}
fn draw_image_at(
&mut self,
rect: Rect,
image: ImageBitmap,
alpha: f32,
color_filter: Option<ColorFilter>,
) {
self.draw_image_at_sampled(rect, image, alpha, color_filter, ImageSampling::Nearest);
}
fn draw_image_at_sampled(
&mut self,
rect: Rect,
image: ImageBitmap,
alpha: f32,
color_filter: Option<ColorFilter>,
sampling: ImageSampling,
) {
self.push_blended_primitive(
DrawPrimitive::Image {
rect,
image,
alpha: alpha.clamp(0.0, 1.0),
color_filter,
sampling,
src_rect: None,
},
BlendMode::SrcOver,
);
}
fn draw_image_at_blend(
&mut self,
rect: Rect,
image: ImageBitmap,
alpha: f32,
color_filter: Option<ColorFilter>,
blend_mode: BlendMode,
) {
self.push_blended_primitive(
DrawPrimitive::Image {
rect,
image,
alpha: alpha.clamp(0.0, 1.0),
color_filter,
sampling: ImageSampling::Nearest,
src_rect: None,
},
blend_mode,
);
}
fn draw_image_src(
&mut self,
image: ImageBitmap,
src_rect: Rect,
dst_rect: Rect,
alpha: f32,
color_filter: Option<ColorFilter>,
) {
self.draw_image_src_blend(
image,
src_rect,
dst_rect,
alpha,
color_filter,
BlendMode::SrcOver,
);
}
fn draw_image_src_sampled(
&mut self,
image: ImageBitmap,
src_rect: Rect,
dst_rect: Rect,
alpha: f32,
color_filter: Option<ColorFilter>,
sampling: ImageSampling,
) {
self.push_blended_primitive(
DrawPrimitive::Image {
rect: dst_rect,
image,
alpha: alpha.clamp(0.0, 1.0),
color_filter,
sampling,
src_rect: Some(src_rect),
},
BlendMode::SrcOver,
);
}
fn draw_image_src_blend(
&mut self,
image: ImageBitmap,
src_rect: Rect,
dst_rect: Rect,
alpha: f32,
color_filter: Option<ColorFilter>,
blend_mode: BlendMode,
) {
self.push_blended_primitive(
DrawPrimitive::Image {
rect: dst_rect,
image,
alpha: alpha.clamp(0.0, 1.0),
color_filter,
sampling: ImageSampling::Nearest,
src_rect: Some(src_rect),
},
blend_mode,
);
}
fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush) {
const SUPERSAMPLE: f32 = 2.0;
const MAX_MASK_PIXELS: f32 = 4096.0;
if path.is_empty() {
return;
}
let bounds = path.bounds();
if bounds.width <= 0.0 || bounds.height <= 0.0 {
return;
}
let color = match &brush {
Brush::Solid(color) => *color,
Brush::LinearGradient { colors, .. }
| Brush::RadialGradient { colors, .. }
| Brush::SweepGradient { colors, .. } => match colors.first() {
Some(color) => *color,
None => return,
},
};
if color.3 <= 0.0 {
return;
}
let origin = Point::new(bounds.x.floor() - 1.0, bounds.y.floor() - 1.0);
let rect_width = (bounds.x + bounds.width).ceil() - origin.x + 1.0;
let rect_height = (bounds.y + bounds.height).ceil() - origin.y + 1.0;
let mask_width = (rect_width * SUPERSAMPLE)
.ceil()
.clamp(1.0, MAX_MASK_PIXELS) as usize;
let mask_height = (rect_height * SUPERSAMPLE)
.ceil()
.clamp(1.0, MAX_MASK_PIXELS) as usize;
let mask = path.coverage_mask(mask_width, mask_height, origin, SUPERSAMPLE);
let red = (color.0.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
let green = (color.1.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
let blue = (color.2.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
let alpha = color.3.clamp(0.0, 1.0);
let mut pixels = Vec::with_capacity(mask.len() * 4);
for coverage in mask {
pixels.extend_from_slice(&[red, green, blue, (alpha * coverage as f32 + 0.5) as u8]);
}
let Ok(image) = ImageBitmap::from_rgba8(mask_width as u32, mask_height as u32, pixels)
else {
return;
};
self.push_other(DrawPrimitive::Image {
rect: Rect {
x: origin.x,
y: origin.y,
width: rect_width,
height: rect_height,
},
image,
alpha: 1.0,
color_filter: None,
sampling: ImageSampling::Linear,
src_rect: None,
});
}
fn measure_text(&self, text: &str, style: &TextStyle) -> TextMeasurement {
match &self.text_measurer {
Some(measurer) => measurer.measure_text(text, style),
None => estimate_text_measurement(text, style),
}
}
fn draw_text_at(&mut self, rect: Rect, brush: Brush, text: &str, style: &TextStyle) {
if text.is_empty() {
return;
}
let Some(color) = solid_fill_color(&brush) else {
return;
};
if color.3 <= 0.0 {
return;
}
let measurement = self.measure_text(text, style);
if !(measurement.size.width > 0.0 && measurement.size.height > 0.0) {
return;
}
let origin = align_text_block(rect, measurement, style);
if !origin.x.is_finite() || !origin.y.is_finite() {
return;
}
self.push_other(DrawPrimitive::Text(Box::new(TextPrimitive {
rect: Rect::from_origin_size(origin, measurement.size),
text: shared_text_str(text),
style: style.clone(),
color,
})));
}
fn into_primitives(self) -> Vec<DrawPrimitive> {
self.finish().primitives
}
}
fn solid_fill_color(brush: &Brush) -> Option<Color> {
match brush {
Brush::Solid(color) => Some(*color),
Brush::LinearGradient { colors, .. }
| Brush::RadialGradient { colors, .. }
| Brush::SweepGradient { colors, .. } => colors.first().copied(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Color, FontStyle, FontWeight, ImageBitmap, RenderEffect};
#[test]
fn compact_recording_materializes_in_recorded_order() {
let size = Size::new(100.0, 100.0);
let solid = Brush::solid(Color::WHITE);
let gradient = Brush::vertical_gradient(vec![Color::RED, Color::BLUE], 0.0, 100.0);
let center = Point::new(50.0, 50.0);
let stroke = Stroke::new(4.0);
let rect = Rect {
x: 10.0,
y: 20.0,
width: 30.0,
height: 40.0,
};
let batch = vec![
DrawPrimitive::Content,
DrawPrimitive::Rect {
rect,
brush: solid.clone(),
stroke: None,
},
];
let record = |scope: &mut DrawScopeDefault| {
scope.draw_rect_at(rect, solid.clone());
scope.draw_arc(solid.clone(), center, 30.0, 0.5, 1.5, stroke);
scope.draw_rect_at(rect, gradient.clone());
scope.draw_circle(solid.clone(), center, 12.0);
scope.draw_arc(solid.clone(), center, 30.0, 0.5, 0.0, stroke); scope.draw_rect_at_blend(rect, solid.clone(), BlendMode::Plus);
scope.draw_content();
scope.draw_annular_sector(gradient.clone(), center, 10.0, 20.0, 0.0, 2.0);
scope.push_recorded(batch.clone());
};
let mut compact = DrawScopeDefault::new(size);
record(&mut compact);
let finished = compact.finish();
let arc_via_ordinary = |brush: Brush, radius: f32, start: f32, sweep: f32| {
let mut scope = DrawScopeDefault::new(size);
scope.draw_arc(brush, center, radius, start, sweep, stroke);
scope.into_primitives().remove(0)
};
let expected = vec![
DrawPrimitive::Rect {
rect,
brush: solid.clone(),
stroke: None,
},
arc_via_ordinary(solid.clone(), 30.0, 0.5, 1.5),
DrawPrimitive::Rect {
rect,
brush: gradient.clone(),
stroke: None,
},
DrawPrimitive::RoundRect {
rect: Rect {
x: center.x - 12.0,
y: center.y - 12.0,
width: 24.0,
height: 24.0,
},
brush: solid.clone(),
radii: CornerRadii::uniform(12.0),
stroke: None,
},
DrawPrimitive::Blend {
primitive: Box::new(DrawPrimitive::Rect {
rect,
brush: solid.clone(),
stroke: None,
}),
blend_mode: BlendMode::Plus,
},
DrawPrimitive::Content,
{
let mut scope = DrawScopeDefault::new(size);
scope.draw_annular_sector(gradient.clone(), center, 10.0, 20.0, 0.0, 2.0);
scope.into_primitives().remove(0)
},
DrawPrimitive::Content,
DrawPrimitive::Rect {
rect,
brush: solid.clone(),
stroke: None,
},
];
assert_eq!(finished.primitives, expected);
assert_eq!(finished.content_markers, 2);
}
#[test]
fn reused_recording_buffers_record_identically_to_fresh() {
let size = Size::new(64.0, 64.0);
let record = |scope: &mut DrawScopeDefault| {
scope.draw_circle(Brush::solid(Color::RED), Point::new(32.0, 32.0), 10.0);
scope.draw_arc(
Brush::solid(Color::BLUE),
Point::new(32.0, 32.0),
20.0,
0.0,
3.0,
Stroke::new(2.0),
);
};
let mut fresh = DrawScopeDefault::new(size);
record(&mut fresh);
let fresh = fresh.finish();
let mut dirty = DrawScopeDefault::new(size);
dirty.draw_rect(Brush::solid(Color::BLACK));
dirty.draw_content();
dirty.draw_arc(
Brush::solid(Color::WHITE),
Point::new(1.0, 1.0),
5.0,
1.0,
1.0,
Stroke::new(1.0),
);
let dirty = dirty.finish();
let mut reused =
DrawScopeDefault::with_recording(size, None, dirty.recording, dirty.primitives);
record(&mut reused);
let reused = reused.finish();
assert_eq!(fresh.primitives, reused.primitives);
assert_eq!(fresh.content_markers, reused.content_markers);
}
#[test]
fn redrawing_the_same_text_shares_one_str_allocation() {
let first = shared_text_str("BREAK THE RING");
let second = shared_text_str("BREAK THE RING");
assert!(Rc::ptr_eq(&first, &second));
assert_eq!(&*second, "BREAK THE RING");
}
#[test]
fn different_text_gets_its_own_str() {
let first = shared_text_str("340");
let second = shared_text_str("350");
assert!(!Rc::ptr_eq(&first, &second));
assert_eq!(&*first, "340");
assert_eq!(&*second, "350");
}
#[test]
fn the_text_pool_survives_overflowing_its_capacity() {
for index in 0..600 {
let text = format!("run-{index}");
assert_eq!(&*shared_text_str(&text), text.as_str());
}
assert_eq!(&*shared_text_str("still correct"), "still correct");
}
fn assert_image_alpha(primitive: &DrawPrimitive, expected: f32) {
match primitive {
DrawPrimitive::Image { alpha, .. } => assert!((alpha - expected).abs() < 1e-5),
DrawPrimitive::Blend { primitive, .. } => assert_image_alpha(primitive, expected),
other => panic!("expected image primitive, got {other:?}"),
}
}
fn unwrap_image(primitive: &DrawPrimitive) -> &DrawPrimitive {
match primitive {
DrawPrimitive::Image { .. } => primitive,
DrawPrimitive::Blend { primitive, .. } => unwrap_image(primitive),
other => panic!("expected image primitive, got {other:?}"),
}
}
#[test]
fn draw_svg_path_emits_supersampled_image_over_path_bounds() {
let mut scope = DrawScopeDefault::new(Size::new(32.0, 32.0));
scope.draw_svg_path("M 4 4 H 20 V 20 H 4 Z", Brush::solid(Color::RED));
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 1);
let DrawPrimitive::Image { rect, image, .. } = &primitives[0] else {
panic!("expected image primitive, got {:?}", primitives[0]);
};
assert_eq!((rect.x, rect.y), (3.0, 3.0));
assert_eq!((rect.width, rect.height), (18.0, 18.0));
assert_eq!((image.width(), image.height()), (36, 36));
let pixels = image.pixels();
let index = (18 * 36 + 18) * 4;
assert_eq!(
&pixels[index..index + 4],
&[255, 0, 0, 255],
"path interior must be opaque brush color"
);
assert_eq!(pixels[3], 0, "outside the path must stay transparent");
}
#[test]
fn draw_svg_path_ignores_invalid_data() {
let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
scope.draw_svg_path("definitely not a path", Brush::solid(Color::WHITE));
assert!(scope.into_primitives().is_empty());
}
#[test]
fn draw_vector_path_applies_brush_alpha() {
let path = crate::VectorPath::parse("M 0 0 H 8 V 8 H 0 Z").expect("valid path");
let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
scope.draw_vector_path(&path, Brush::solid(Color::rgba(0.0, 0.0, 1.0, 0.5)));
let primitives = scope.into_primitives();
let DrawPrimitive::Image { image, .. } = &primitives[0] else {
panic!("expected image primitive");
};
let pixels = image.pixels();
let width = image.width() as usize;
let index = ((image.height() as usize / 2) * width + width / 2) * 4;
assert_eq!(&pixels[index..index + 3], &[0, 0, 255]);
let alpha = pixels[index + 3];
assert!(
(alpha as i32 - 128).abs() <= 2,
"interior alpha must honor the brush alpha, got {alpha}"
);
}
#[test]
fn draw_content_inserts_content_marker() {
let mut scope = DrawScopeDefault::new(Size::new(8.0, 8.0));
scope.draw_rect(Brush::solid(Color::WHITE));
scope.draw_content();
scope.draw_rect_blend(Brush::solid(Color::BLACK), BlendMode::DstOut);
let primitives = scope.into_primitives();
assert!(matches!(primitives[1], DrawPrimitive::Content));
assert!(matches!(
primitives[2],
DrawPrimitive::Blend {
blend_mode: BlendMode::DstOut,
..
}
));
}
#[test]
fn draw_rect_blend_wraps_non_default_modes() {
let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
scope.draw_rect_blend(Brush::solid(Color::RED), BlendMode::DstOut);
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 1);
match &primitives[0] {
DrawPrimitive::Blend {
primitive,
blend_mode,
} => {
assert_eq!(*blend_mode, BlendMode::DstOut);
assert!(matches!(**primitive, DrawPrimitive::Rect { .. }));
}
other => panic!("expected blended primitive, got {other:?}"),
}
}
#[test]
fn draw_circle_records_centered_round_rect() {
let mut scope = DrawScopeDefault::new(Size::new(40.0, 40.0));
scope.draw_circle(Brush::solid(Color::BLUE), Point::new(12.0, 16.0), 5.0);
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 1);
match &primitives[0] {
DrawPrimitive::RoundRect { rect, radii, .. } => {
assert_eq!(
*rect,
Rect {
x: 7.0,
y: 11.0,
width: 10.0,
height: 10.0,
}
);
assert_eq!(*radii, CornerRadii::uniform(5.0));
}
other => panic!("expected circular round rect, got {other:?}"),
}
}
#[test]
fn draw_circle_blend_wraps_non_default_modes() {
let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
scope.draw_circle_blend(
Brush::solid(Color::RED),
Point::new(5.0, 5.0),
3.0,
BlendMode::Plus,
);
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 1);
match &primitives[0] {
DrawPrimitive::Blend {
primitive,
blend_mode,
} => {
assert_eq!(*blend_mode, BlendMode::Plus);
assert!(matches!(**primitive, DrawPrimitive::RoundRect { .. }));
}
other => panic!("expected blended circle primitive, got {other:?}"),
}
}
#[test]
fn rect_union_encloses_both_inputs() {
let lhs = Rect {
x: 10.0,
y: 5.0,
width: 8.0,
height: 4.0,
};
let rhs = Rect {
x: 4.0,
y: 7.0,
width: 10.0,
height: 6.0,
};
assert_eq!(
lhs.union(rhs),
Rect {
x: 4.0,
y: 5.0,
width: 14.0,
height: 8.0,
}
);
}
#[test]
fn draw_image_uses_scope_size_as_default_rect() {
let mut scope = DrawScopeDefault::new(Size::new(40.0, 24.0));
let image = ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
scope.draw_image(image.clone());
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 1);
match unwrap_image(&primitives[0]) {
DrawPrimitive::Image {
rect,
image: actual,
alpha,
color_filter,
sampling,
src_rect,
} => {
assert_eq!(*rect, Rect::from_size(Size::new(40.0, 24.0)));
assert_eq!(*actual, image);
assert_eq!(*alpha, 1.0);
assert!(color_filter.is_none());
assert_eq!(*sampling, ImageSampling::Nearest);
assert!(src_rect.is_none());
}
other => panic!("expected image primitive, got {other:?}"),
}
}
#[test]
fn draw_image_src_stores_src_rect() {
let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
let src = Rect {
x: 10.0,
y: 20.0,
width: 30.0,
height: 40.0,
};
let dst = Rect {
x: 0.0,
y: 0.0,
width: 60.0,
height: 80.0,
};
scope.draw_image_src(image.clone(), src, dst, 0.8, None);
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 1);
match unwrap_image(&primitives[0]) {
DrawPrimitive::Image {
rect,
image: actual,
alpha,
sampling,
src_rect,
..
} => {
assert_eq!(*rect, dst);
assert_eq!(*actual, image);
assert!((alpha - 0.8).abs() < 1e-5);
assert_eq!(*sampling, ImageSampling::Nearest);
assert_eq!(*src_rect, Some(src));
}
other => panic!("expected image primitive, got {other:?}"),
}
}
#[test]
fn draw_image_at_sampled_records_requested_sampling() {
let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
let image = ImageBitmap::from_rgba8(8, 8, vec![255; 8 * 8 * 4]).expect("image");
let dst = Rect {
x: 2.0,
y: 3.0,
width: 40.0,
height: 30.0,
};
scope.draw_image_at_sampled(dst, image.clone(), 0.7, None, ImageSampling::Linear);
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 1);
match unwrap_image(&primitives[0]) {
DrawPrimitive::Image {
rect,
image: actual,
alpha,
sampling,
src_rect,
..
} => {
assert_eq!(*rect, dst);
assert_eq!(*actual, image);
assert!((alpha - 0.7).abs() < 1e-5);
assert_eq!(*sampling, ImageSampling::Linear);
assert!(src_rect.is_none());
}
other => panic!("expected image primitive, got {other:?}"),
}
}
#[test]
fn draw_image_src_sampled_records_requested_sampling() {
let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
let src = Rect {
x: 4.0,
y: 6.0,
width: 16.0,
height: 20.0,
};
let dst = Rect {
x: 8.0,
y: 10.0,
width: 32.0,
height: 40.0,
};
scope.draw_image_src_sampled(image.clone(), src, dst, 0.5, None, ImageSampling::Linear);
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 1);
match unwrap_image(&primitives[0]) {
DrawPrimitive::Image {
rect,
image: actual,
alpha,
sampling,
src_rect,
..
} => {
assert_eq!(*rect, dst);
assert_eq!(*actual, image);
assert!((alpha - 0.5).abs() < 1e-5);
assert_eq!(*sampling, ImageSampling::Linear);
assert_eq!(*src_rect, Some(src));
}
other => panic!("expected image primitive, got {other:?}"),
}
}
#[test]
fn draw_image_at_clamps_alpha() {
let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
let image = ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image");
scope.draw_image_at(
Rect::from_origin_size(Point::new(2.0, 3.0), Size::new(5.0, 6.0)),
image,
3.0,
Some(ColorFilter::Tint(Color::from_rgba_u8(128, 128, 255, 255))),
);
assert_image_alpha(&scope.into_primitives()[0], 1.0);
}
#[test]
fn graphics_layer_clone_with_render_effect() {
let layer = GraphicsLayer {
render_effect: Some(RenderEffect::blur(10.0)),
backdrop_effect: Some(RenderEffect::blur(6.0)),
color_filter: Some(ColorFilter::tint(Color::from_rgba_u8(128, 200, 255, 255))),
alpha: 0.5,
rotation_z: 12.0,
shadow_elevation: 4.0,
shape: LayerShape::Rounded(RoundedCornerShape::uniform(6.0)),
clip: true,
compositing_strategy: CompositingStrategy::Offscreen,
blend_mode: BlendMode::SrcOver,
..Default::default()
};
let cloned = layer.clone();
assert_eq!(cloned.alpha, 0.5);
assert!(cloned.render_effect.is_some());
assert!(cloned.backdrop_effect.is_some());
assert_eq!(layer.color_filter, cloned.color_filter);
assert_eq!(layer.render_effect, cloned.render_effect);
assert_eq!(layer.backdrop_effect, cloned.backdrop_effect);
assert!((cloned.rotation_z - 12.0).abs() < 1e-6);
assert!((cloned.shadow_elevation - 4.0).abs() < 1e-6);
assert_eq!(
cloned.shape,
LayerShape::Rounded(RoundedCornerShape::uniform(6.0))
);
assert!(cloned.clip);
assert_eq!(cloned.compositing_strategy, CompositingStrategy::Offscreen);
assert_eq!(cloned.blend_mode, BlendMode::SrcOver);
}
#[test]
fn graphics_layer_default_has_no_effect() {
let layer = GraphicsLayer::default();
assert!(layer.color_filter.is_none());
assert!(layer.render_effect.is_none());
assert!(layer.backdrop_effect.is_none());
assert_eq!(layer.compositing_strategy, CompositingStrategy::Auto);
assert_eq!(layer.blend_mode, BlendMode::SrcOver);
assert_eq!(layer.alpha, 1.0);
assert_eq!(layer.transform_origin, TransformOrigin::CENTER);
assert!((layer.camera_distance - 8.0).abs() < 1e-6);
assert_eq!(layer.shape, LayerShape::Rectangle);
assert!(!layer.clip);
assert_eq!(layer.ambient_shadow_color, Color::BLACK);
assert_eq!(layer.spot_shadow_color, Color::BLACK);
}
#[test]
fn transform_origin_construction() {
let origin = TransformOrigin::new(0.25, 0.75);
assert!((origin.pivot_fraction_x - 0.25).abs() < 1e-6);
assert!((origin.pivot_fraction_y - 0.75).abs() < 1e-6);
}
#[test]
fn layer_shape_default_is_rectangle() {
assert_eq!(LayerShape::default(), LayerShape::Rectangle);
}
use crate::{StrokeCap, StrokeJoin};
use std::f32::consts::{FRAC_PI_2, PI};
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 0.25
}
fn scope(size: f32) -> DrawScopeDefault {
DrawScopeDefault::new(Size::new(size, size))
}
#[test]
fn draw_rect_stroked_records_scope_rect_and_stroke() {
let mut scope = scope(20.0);
scope.draw_rect_stroked(
Brush::solid(Color::RED),
Stroke::new(3.0).with_join(StrokeJoin::Bevel),
);
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 1);
match &primitives[0] {
DrawPrimitive::Rect {
rect,
stroke: Some(stroke),
..
} => {
assert_eq!(*rect, Rect::from_size(Size::new(20.0, 20.0)));
assert_eq!(stroke.width, 3.0);
assert_eq!(stroke.join, StrokeJoin::Bevel);
}
other => panic!("expected stroked rect, got {other:?}"),
}
}
#[test]
fn draw_rect_at_stroked_records_requested_rect() {
let mut scope = scope(50.0);
let rect = Rect {
x: 4.0,
y: 6.0,
width: 12.0,
height: 9.0,
};
scope.draw_rect_at_stroked(rect, Brush::solid(Color::BLUE), Stroke::new(2.0));
match &scope.into_primitives()[0] {
DrawPrimitive::Rect {
rect: actual,
stroke: Some(stroke),
..
} => {
assert_eq!(*actual, rect);
assert_eq!(stroke.width, 2.0);
}
other => panic!("expected stroked rect, got {other:?}"),
}
}
#[test]
fn draw_round_rect_stroked_keeps_radii_and_stroke() {
let mut scope = scope(30.0);
scope.draw_round_rect_stroked(
Brush::solid(Color::GREEN),
CornerRadii::uniform(5.0),
Stroke::new(4.0).with_join(StrokeJoin::Round),
);
match &scope.into_primitives()[0] {
DrawPrimitive::RoundRect {
rect,
radii,
stroke: Some(stroke),
..
} => {
assert_eq!(*rect, Rect::from_size(Size::new(30.0, 30.0)));
assert_eq!(*radii, CornerRadii::uniform(5.0));
assert_eq!(stroke.width, 4.0);
assert_eq!(stroke.join, StrokeJoin::Round);
}
other => panic!("expected stroked round rect, got {other:?}"),
}
}
#[test]
fn draw_round_rect_at_stroked_records_requested_rect() {
let mut scope = scope(60.0);
let rect = Rect {
x: 1.0,
y: 2.0,
width: 20.0,
height: 10.0,
};
scope.draw_round_rect_at_stroked(
rect,
Brush::solid(Color::WHITE),
CornerRadii::uniform(3.0),
Stroke::new(1.5),
);
match &scope.into_primitives()[0] {
DrawPrimitive::RoundRect {
rect: actual,
radii,
stroke: Some(stroke),
..
} => {
assert_eq!(*actual, rect);
assert_eq!(*radii, CornerRadii::uniform(3.0));
assert_eq!(stroke.width, 1.5);
}
other => panic!("expected stroked round rect, got {other:?}"),
}
}
#[test]
fn draw_circle_stroked_lowers_to_stroked_round_rect() {
let mut scope = scope(40.0);
scope.draw_circle_stroked(
Brush::solid(Color::BLUE),
Point::new(12.0, 16.0),
5.0,
Stroke::new(2.0),
);
match &scope.into_primitives()[0] {
DrawPrimitive::RoundRect {
rect,
radii,
stroke: Some(stroke),
..
} => {
assert_eq!(
*rect,
Rect {
x: 7.0,
y: 11.0,
width: 10.0,
height: 10.0,
}
);
assert_eq!(*radii, CornerRadii::uniform(5.0));
assert_eq!(stroke.width, 2.0);
}
other => panic!("expected stroked circular round rect, got {other:?}"),
}
}
#[test]
fn draw_arc_records_arc_primitive_with_tight_bounds() {
let mut scope = scope(200.0);
scope.draw_arc(
Brush::solid(Color::RED),
Point::new(100.0, 100.0),
50.0,
0.0,
FRAC_PI_2,
Stroke::new(10.0),
);
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 1);
match &primitives[0] {
DrawPrimitive::Arc {
rect,
center,
radius,
start_angle,
sweep_angle,
stroke: Some(stroke),
inner_radius,
..
} => {
assert_eq!(*center, Point::new(100.0, 100.0));
assert_eq!(*radius, 50.0);
assert_eq!(*start_angle, 0.0);
assert!(approx(*sweep_angle, FRAC_PI_2));
assert_eq!(stroke.width, 10.0);
assert_eq!(*inner_radius, 0.0);
assert!(approx(rect.x, 100.0), "{rect:?}");
assert!(approx(rect.y, 100.0), "{rect:?}");
assert!(approx(rect.width, 55.0), "{rect:?}");
assert!(approx(rect.height, 55.0), "{rect:?}");
}
other => panic!("expected arc primitive, got {other:?}"),
}
}
#[test]
fn draw_arc_bounds_cover_a_quadrant_spanning_sweep() {
let mut scope = scope(200.0);
scope.draw_arc(
Brush::solid(Color::RED),
Point::new(100.0, 100.0),
50.0,
0.0,
3.0 * FRAC_PI_2,
Stroke::new(4.0),
);
let DrawPrimitive::Arc { rect, .. } = &scope.into_primitives()[0] else {
panic!("expected arc primitive");
};
assert!(approx(rect.x, 48.0), "{rect:?}");
assert!(approx(rect.y, 48.0), "{rect:?}");
assert!(approx(rect.width, 104.0), "{rect:?}");
assert!(approx(rect.height, 104.0), "{rect:?}");
}
#[test]
fn draw_annular_sector_records_inner_radius_and_no_stroke() {
let mut scope = scope(200.0);
scope.draw_annular_sector(
Brush::solid(Color::WHITE),
Point::new(100.0, 100.0),
30.0,
50.0,
0.0,
PI,
);
match &scope.into_primitives()[0] {
DrawPrimitive::Arc {
rect,
center,
radius,
inner_radius,
stroke,
sweep_angle,
..
} => {
assert!(stroke.is_none(), "annular sectors are filled, not stroked");
assert_eq!(*center, Point::new(100.0, 100.0));
assert_eq!(*radius, 50.0);
assert_eq!(*inner_radius, 30.0);
assert!(approx(*sweep_angle, PI));
assert!(approx(rect.x, 50.0), "{rect:?}");
assert!(approx(rect.y, 100.0), "{rect:?}");
assert!(approx(rect.width, 100.0), "{rect:?}");
assert!(approx(rect.height, 50.0), "{rect:?}");
}
other => panic!("expected arc primitive, got {other:?}"),
}
}
#[test]
fn draw_arc_blend_wraps_non_default_modes() {
let mut scope = scope(100.0);
scope.draw_arc_blend(
Brush::solid(Color::RED),
Point::new(50.0, 50.0),
20.0,
0.0,
1.0,
Stroke::new(2.0),
BlendMode::DstOut,
);
match &scope.into_primitives()[0] {
DrawPrimitive::Blend {
primitive,
blend_mode,
} => {
assert_eq!(*blend_mode, BlendMode::DstOut);
assert!(matches!(**primitive, DrawPrimitive::Arc { .. }));
}
other => panic!("expected blended arc, got {other:?}"),
}
}
#[test]
fn draw_annular_sector_blend_wraps_non_default_modes() {
let mut scope = scope(100.0);
scope.draw_annular_sector_blend(
Brush::solid(Color::RED),
Point::new(50.0, 50.0),
5.0,
20.0,
0.0,
1.0,
BlendMode::Plus,
);
assert!(matches!(
&scope.into_primitives()[0],
DrawPrimitive::Blend {
blend_mode: BlendMode::Plus,
..
}
));
}
#[test]
fn stroked_blend_variants_wrap_non_default_modes() {
let mut scope = scope(20.0);
scope.draw_rect_stroked_blend(
Brush::solid(Color::RED),
Stroke::new(2.0),
BlendMode::DstOut,
);
scope.draw_round_rect_stroked_blend(
Brush::solid(Color::RED),
CornerRadii::uniform(2.0),
Stroke::new(2.0),
BlendMode::DstOut,
);
scope.draw_circle_stroked_blend(
Brush::solid(Color::RED),
Point::new(10.0, 10.0),
5.0,
Stroke::new(2.0),
BlendMode::DstOut,
);
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 3);
for primitive in &primitives {
assert!(
matches!(
primitive,
DrawPrimitive::Blend {
blend_mode: BlendMode::DstOut,
..
}
),
"expected blended primitive, got {primitive:?}"
);
}
}
#[test]
fn negative_sweeps_and_overlong_sweeps_produce_finite_bounds() {
let mut scope = scope(200.0);
scope.draw_arc(
Brush::solid(Color::RED),
Point::new(100.0, 100.0),
40.0,
FRAC_PI_2,
-FRAC_PI_2,
Stroke::new(4.0),
);
scope.draw_arc(
Brush::solid(Color::RED),
Point::new(100.0, 100.0),
40.0,
0.3,
crate::stroke::TAU * 4.0,
Stroke::new(4.0),
);
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 2);
let DrawPrimitive::Arc { rect: negative, .. } = &primitives[0] else {
panic!("expected arc");
};
assert!(approx(negative.x, 100.0), "{negative:?}");
assert!(approx(negative.y, 100.0), "{negative:?}");
assert!(approx(negative.width, 42.0), "{negative:?}");
let DrawPrimitive::Arc { rect: full, .. } = &primitives[1] else {
panic!("expected arc");
};
assert!(approx(full.x, 58.0), "{full:?}");
assert!(approx(full.width, 84.0), "{full:?}");
assert!(approx(full.height, 84.0), "{full:?}");
}
#[test]
fn degenerate_stroke_and_arc_inputs_emit_nothing_and_never_panic() {
let mut scope = scope(50.0);
let brush = Brush::solid(Color::RED);
let center = Point::new(25.0, 25.0);
scope.draw_rect_stroked(brush.clone(), Stroke::new(0.0));
scope.draw_rect_stroked(brush.clone(), Stroke::new(-4.0));
scope.draw_rect_stroked(brush.clone(), Stroke::new(f32::NAN));
scope.draw_round_rect_stroked(brush.clone(), CornerRadii::uniform(2.0), Stroke::new(0.0));
scope.draw_circle_stroked(brush.clone(), center, 10.0, Stroke::new(0.0));
scope.draw_circle_stroked(brush.clone(), center, f32::NAN, Stroke::new(2.0));
scope.draw_arc(brush.clone(), center, 10.0, 0.0, 0.0, Stroke::new(2.0));
scope.draw_arc(brush.clone(), center, 10.0, 0.0, f32::NAN, Stroke::new(2.0));
scope.draw_arc(
brush.clone(),
center,
f32::INFINITY,
0.0,
1.0,
Stroke::new(2.0),
);
scope.draw_arc(brush.clone(), center, 10.0, 0.0, 1.0, Stroke::new(0.0));
scope.draw_arc(brush.clone(), center, 0.0, 0.0, 1.0, Stroke::new(0.0));
scope.draw_annular_sector(brush.clone(), center, 10.0, 10.0, 0.0, 1.0);
scope.draw_annular_sector(brush.clone(), center, 20.0, 10.0, 0.0, 1.0);
scope.draw_annular_sector(brush.clone(), center, 0.0, 0.0, 0.0, 1.0);
scope.draw_annular_sector(brush.clone(), center, 0.0, 10.0, 0.0, 0.0);
scope.draw_annular_sector(brush, center, f32::NAN, 10.0, 0.0, 1.0);
assert!(
scope.into_primitives().is_empty(),
"degenerate stroke/arc requests must not reach the renderer"
);
}
#[test]
fn zero_radius_arc_with_positive_width_stays_finite() {
let mut scope = scope(50.0);
scope.draw_arc(
Brush::solid(Color::RED),
Point::new(25.0, 25.0),
0.0,
0.0,
FRAC_PI_2,
Stroke::new(6.0).with_cap(StrokeCap::Round),
);
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 1);
let DrawPrimitive::Arc { rect, .. } = &primitives[0] else {
panic!("expected arc");
};
for value in [rect.x, rect.y, rect.width, rect.height] {
assert!(value.is_finite(), "{rect:?}");
}
assert!(rect.width > 0.0 && rect.height > 0.0, "{rect:?}");
}
struct FixedAdvanceTextMeasurer {
advance: f32,
line_height: f32,
calls: std::cell::Cell<usize>,
}
impl FixedAdvanceTextMeasurer {
fn shared(advance: f32, line_height: f32) -> Rc<Self> {
Rc::new(Self {
advance,
line_height,
calls: std::cell::Cell::new(0),
})
}
}
impl DrawTextMeasurer for FixedAdvanceTextMeasurer {
fn measure_text(&self, text: &str, _style: &TextStyle) -> TextMeasurement {
self.calls.set(self.calls.get() + 1);
let lines: Vec<&str> = text.split('\n').collect();
let width = lines
.iter()
.map(|line| line.chars().count() as f32 * self.advance)
.fold(0.0_f32, f32::max);
TextMeasurement {
size: Size::new(width, lines.len() as f32 * self.line_height),
line_height: self.line_height,
first_baseline: self.line_height * 0.75,
line_count: lines.len(),
}
}
}
fn text_scope(size: Size) -> (DrawScopeDefault, Rc<FixedAdvanceTextMeasurer>) {
let measurer = FixedAdvanceTextMeasurer::shared(10.0, 20.0);
(
DrawScopeDefault::with_text_measurer(size, measurer.clone()),
measurer,
)
}
fn unwrap_text(primitive: &DrawPrimitive) -> &TextPrimitive {
match primitive {
DrawPrimitive::Text(text) => text,
other => panic!("expected text primitive, got {other:?}"),
}
}
#[test]
fn drawn_text_occupies_exactly_the_box_measure_text_reported() {
let (mut scope, _) = text_scope(Size::new(200.0, 100.0));
let style = TextStyle::new(16.0);
let measured = scope.measure_text("ABCD", &style);
scope.draw_text_from(
Point::new(7.0, 11.0),
Brush::solid(Color::WHITE),
"ABCD",
&style,
);
let primitives = scope.into_primitives();
assert_eq!(primitives.len(), 1);
let text = unwrap_text(&primitives[0]);
assert_eq!(
text.rect,
Rect {
x: 7.0,
y: 11.0,
width: measured.size.width,
height: measured.size.height,
},
"the drawn block must be the measured block, or callers cannot center text"
);
assert_eq!(&*text.text, "ABCD");
assert_eq!(text.color, Color::WHITE);
}
#[test]
fn text_alignment_positions_the_measured_block_inside_the_box() {
let box_rect = Rect {
x: 100.0,
y: 50.0,
width: 200.0,
height: 80.0,
};
let cases = [
(TextAlign::Left, TextVerticalAlign::Top, 100.0, 50.0),
(TextAlign::Center, TextVerticalAlign::Center, 190.0, 80.0),
(TextAlign::Right, TextVerticalAlign::Bottom, 280.0, 110.0),
];
for (align, vertical_align, expected_x, expected_y) in cases {
let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
let style = TextStyle::new(16.0)
.with_align(align)
.with_vertical_align(vertical_align);
scope.draw_text_at(box_rect, Brush::solid(Color::WHITE), "AB", &style);
let primitives = scope.into_primitives();
let text = unwrap_text(&primitives[0]);
assert!(
approx(text.rect.x, expected_x) && approx(text.rect.y, expected_y),
"{align:?}/{vertical_align:?} placed the block at {:?}",
text.rect
);
assert!(approx(text.rect.width, 20.0) && approx(text.rect.height, 20.0));
}
}
#[test]
fn baseline_aligned_text_hangs_above_the_box_edge() {
let (mut scope, _) = text_scope(Size::new(200.0, 200.0));
let style = TextStyle::new(16.0).with_vertical_align(TextVerticalAlign::Baseline);
let measured = scope.measure_text("Ag", &style);
scope.draw_text_at(
Rect {
x: 0.0,
y: 100.0,
width: 200.0,
height: 0.0,
},
Brush::solid(Color::WHITE),
"Ag",
&style,
);
let primitives = scope.into_primitives();
let text = unwrap_text(&primitives[0]);
assert!(
approx(text.rect.y, 100.0 - measured.first_baseline),
"{:?}",
text.rect
);
}
#[test]
fn draw_text_fills_the_whole_scope_rect() {
let (mut scope, _) = text_scope(Size::new(120.0, 60.0));
let style = TextStyle::new(16.0)
.with_align(TextAlign::Right)
.with_vertical_align(TextVerticalAlign::Bottom);
scope.draw_text(Brush::solid(Color::WHITE), "AB", &style);
let primitives = scope.into_primitives();
let text = unwrap_text(&primitives[0]);
assert!(
approx(text.rect.x, 100.0) && approx(text.rect.y, 40.0),
"{:?}",
text.rect
);
}
#[test]
fn draw_text_from_ignores_alignment_and_anchors_the_top_left() {
let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
let style = TextStyle::new(16.0)
.with_align(TextAlign::Center)
.with_vertical_align(TextVerticalAlign::Bottom);
scope.draw_text_from(
Point::new(30.0, 40.0),
Brush::solid(Color::WHITE),
"AB",
&style,
);
let primitives = scope.into_primitives();
let text = unwrap_text(&primitives[0]);
assert!(
approx(text.rect.x, 30.0) && approx(text.rect.y, 40.0),
"{:?}",
text.rect
);
}
#[test]
fn multiline_text_measures_the_widest_line_and_stacks_the_lines() {
let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
let style = TextStyle::new(16.0);
scope.draw_text_from(Point::ZERO, Brush::solid(Color::WHITE), "AB\nABCDE", &style);
let primitives = scope.into_primitives();
let text = unwrap_text(&primitives[0]);
assert!(approx(text.rect.width, 50.0), "{:?}", text.rect);
assert!(approx(text.rect.height, 40.0), "{:?}", text.rect);
}
#[test]
fn empty_text_draws_nothing_and_never_measures() {
let (mut scope, measurer) = text_scope(Size::new(100.0, 100.0));
scope.draw_text(Brush::solid(Color::WHITE), "", &TextStyle::new(16.0));
scope.draw_text_at(
Rect::from_size(Size::new(10.0, 10.0)),
Brush::solid(Color::WHITE),
"",
&TextStyle::new(16.0),
);
scope.draw_text_from(
Point::ZERO,
Brush::solid(Color::WHITE),
"",
&TextStyle::new(16.0),
);
assert!(scope.into_primitives().is_empty());
assert_eq!(
measurer.calls.get(),
0,
"an empty string must not cost a measurement"
);
}
#[test]
fn invisible_text_draws_nothing() {
let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
let style = TextStyle::new(16.0);
scope.draw_text(Brush::solid(Color(1.0, 1.0, 1.0, 0.0)), "AB", &style);
scope.draw_text(
Brush::LinearGradient {
colors: Vec::new(),
stops: None,
start: Point::ZERO,
end: Point::new(1.0, 1.0),
tile_mode: crate::render_effect::TileMode::Clamp,
},
"AB",
&style,
);
assert!(scope.into_primitives().is_empty());
}
#[test]
fn gradient_text_brushes_fall_back_to_their_first_stop() {
let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
scope.draw_text(
Brush::linear_gradient(vec![Color::RED, Color::BLUE]),
"AB",
&TextStyle::new(16.0),
);
let primitives = scope.into_primitives();
assert_eq!(unwrap_text(&primitives[0]).color, Color::RED);
}
#[test]
fn a_scope_without_a_measurer_falls_back_to_the_font_free_estimate() {
let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
let style = TextStyle::new(16.0);
assert_eq!(
scope.measure_text("ABC", &style),
crate::estimate_text_measurement("ABC", &style)
);
scope.draw_text_from(Point::ZERO, Brush::solid(Color::WHITE), "ABC", &style);
let primitives = scope.into_primitives();
let text = unwrap_text(&primitives[0]);
assert!(text.rect.width > 0.0 && text.rect.height > 0.0);
}
#[test]
fn degenerate_text_geometry_emits_nothing_and_never_panics() {
struct DegenerateTextMeasurer;
impl DrawTextMeasurer for DegenerateTextMeasurer {
fn measure_text(&self, _text: &str, _style: &TextStyle) -> TextMeasurement {
TextMeasurement {
size: Size::new(f32::NAN, 0.0),
line_height: f32::NAN,
first_baseline: f32::NAN,
line_count: 1,
}
}
}
let mut scope = DrawScopeDefault::with_text_measurer(
Size::new(50.0, 50.0),
Rc::new(DegenerateTextMeasurer),
);
scope.draw_text(Brush::solid(Color::WHITE), "AB", &TextStyle::new(16.0));
scope.draw_text_at(
Rect {
x: f32::NAN,
y: 0.0,
width: 10.0,
height: 10.0,
},
Brush::solid(Color::WHITE),
"AB",
&TextStyle::new(16.0),
);
assert!(
scope.into_primitives().is_empty(),
"unmeasurable text must not reach the renderer"
);
}
#[test]
fn text_style_survives_lowering_into_the_primitive() {
let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
let style = TextStyle::new(21.0)
.with_font_family("Fira Sans")
.with_weight(FontWeight::BOLD)
.with_style(FontStyle::Italic)
.with_letter_spacing(2.0)
.with_line_height(26.0);
scope.draw_text(Brush::solid(Color::WHITE), "AB", &style);
let primitives = scope.into_primitives();
assert_eq!(unwrap_text(&primitives[0]).style, style);
}
}