1248 lines
44 KiB
Rust
1248 lines
44 KiB
Rust
// todo("windows"): remove
|
|
#![cfg_attr(windows, allow(dead_code))]
|
|
|
|
use schemars::JsonSchema;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{
|
|
AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels,
|
|
Point, Radians, ScaledFilter, ScaledPixels, Size, bounds_tree::BoundsTree, point,
|
|
};
|
|
use smallvec::SmallVec;
|
|
use std::{
|
|
fmt::Debug,
|
|
iter::Peekable,
|
|
ops::{Add, Range, Sub},
|
|
slice,
|
|
};
|
|
|
|
#[allow(non_camel_case_types, unused)]
|
|
#[expect(missing_docs)]
|
|
pub type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
|
|
|
|
#[expect(missing_docs)]
|
|
pub type DrawOrder = u32;
|
|
|
|
#[derive(Default)]
|
|
#[expect(missing_docs)]
|
|
pub struct Scene {
|
|
pub(crate) paint_operations: Vec<PaintOperation>,
|
|
primitive_bounds: BoundsTree<ScaledPixels>,
|
|
layer_stack: Vec<DrawOrder>,
|
|
pub shadows: Vec<Shadow>,
|
|
pub quads: Vec<Quad>,
|
|
pub paths: Vec<Path<ScaledPixels>>,
|
|
pub underlines: Vec<Underline>,
|
|
pub monochrome_sprites: Vec<MonochromeSprite>,
|
|
pub subpixel_sprites: Vec<SubpixelSprite>,
|
|
pub polychrome_sprites: Vec<PolychromeSprite>,
|
|
pub surfaces: Vec<PaintSurface>,
|
|
pub backdrop_filters: Vec<BackdropFilter>,
|
|
pub filter_boundaries: Vec<FilterBoundary>,
|
|
}
|
|
|
|
#[expect(missing_docs)]
|
|
impl Scene {
|
|
pub fn clear(&mut self) {
|
|
self.paint_operations.clear();
|
|
self.primitive_bounds.clear();
|
|
self.layer_stack.clear();
|
|
self.paths.clear();
|
|
self.shadows.clear();
|
|
self.quads.clear();
|
|
self.underlines.clear();
|
|
self.monochrome_sprites.clear();
|
|
self.subpixel_sprites.clear();
|
|
self.polychrome_sprites.clear();
|
|
self.surfaces.clear();
|
|
self.backdrop_filters.clear();
|
|
self.filter_boundaries.clear();
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.paint_operations.len()
|
|
}
|
|
|
|
pub fn push_layer(&mut self, bounds: Bounds<ScaledPixels>) {
|
|
let order = self.primitive_bounds.insert(bounds);
|
|
self.layer_stack.push(order);
|
|
self.paint_operations
|
|
.push(PaintOperation::StartLayer(bounds));
|
|
}
|
|
|
|
pub fn pop_layer(&mut self) {
|
|
self.layer_stack.pop();
|
|
self.paint_operations.push(PaintOperation::EndLayer);
|
|
}
|
|
|
|
/// Raise the draw-order floor so every primitive inserted afterwards sorts above everything
|
|
/// inserted before. Called before painting deferred draws so overlays (tooltips, popovers,
|
|
/// drag images) sort above the main scene — and a deferred backdrop's order can't fall inside
|
|
/// a content-filter (`filter`) order range left behind by the main scene.
|
|
pub fn raise_order_floor(&mut self) {
|
|
let floor = self.primitive_bounds.max_order() + 1;
|
|
self.primitive_bounds.set_order_floor(floor);
|
|
}
|
|
|
|
pub fn insert_primitive(&mut self, primitive: impl Into<Primitive>) {
|
|
let mut primitive = primitive.into();
|
|
let clipped_bounds = primitive
|
|
.bounds()
|
|
.intersect(&primitive.content_mask().bounds);
|
|
|
|
// Content-filter boundaries must always be inserted as matched pairs — dropping one
|
|
// (e.g. for an empty clipped region) would orphan its partner and corrupt the renderer's
|
|
// target stack. Each marker takes an order strictly above ALL prior content, so the start
|
|
// sorts after everything painted before it and the element's own children (which overlap
|
|
// the marker bounds) sort strictly above the start. This keeps a marker's order range from
|
|
// colliding with unrelated non-overlapping content that reuses low orderings (e.g. a
|
|
// background grid), which would otherwise sweep that content into the group. Content
|
|
// painted *after* the group is held above it by raising the order floor when the end
|
|
// marker is inserted (see below) — otherwise a later non-overlapping sibling could reuse a
|
|
// low order that lands inside the start..end range and be swept into the group.
|
|
let is_filter_boundary = matches!(primitive, Primitive::FilterBoundary(_));
|
|
|
|
if clipped_bounds.is_empty() && !is_filter_boundary {
|
|
return;
|
|
}
|
|
|
|
let order = if is_filter_boundary {
|
|
let order_bounds = if clipped_bounds.is_empty() {
|
|
*primitive.bounds()
|
|
} else {
|
|
clipped_bounds
|
|
};
|
|
self.primitive_bounds.insert_above_all(order_bounds)
|
|
} else {
|
|
self.layer_stack
|
|
.last()
|
|
.copied()
|
|
.unwrap_or_else(|| self.primitive_bounds.insert(clipped_bounds))
|
|
};
|
|
match &mut primitive {
|
|
Primitive::Shadow(shadow) => {
|
|
shadow.order = order;
|
|
self.shadows.push(*shadow);
|
|
}
|
|
Primitive::Quad(quad) => {
|
|
quad.order = order;
|
|
self.quads.push(*quad);
|
|
}
|
|
Primitive::Path(path) => {
|
|
path.order = order;
|
|
path.id = PathId(self.paths.len());
|
|
self.paths.push(path.clone());
|
|
}
|
|
Primitive::Underline(underline) => {
|
|
underline.order = order;
|
|
self.underlines.push(*underline);
|
|
}
|
|
Primitive::MonochromeSprite(sprite) => {
|
|
sprite.order = order;
|
|
self.monochrome_sprites.push(*sprite);
|
|
}
|
|
Primitive::SubpixelSprite(sprite) => {
|
|
sprite.order = order;
|
|
self.subpixel_sprites.push(*sprite);
|
|
}
|
|
Primitive::PolychromeSprite(sprite) => {
|
|
sprite.order = order;
|
|
self.polychrome_sprites.push(*sprite);
|
|
}
|
|
Primitive::Surface(surface) => {
|
|
surface.order = order;
|
|
self.surfaces.push(surface.clone());
|
|
}
|
|
Primitive::BackdropFilter(filter) => {
|
|
filter.order = order;
|
|
self.backdrop_filters.push(filter.clone());
|
|
}
|
|
Primitive::FilterBoundary(boundary) => {
|
|
boundary.order = order;
|
|
if !boundary.is_start {
|
|
// A closed content-filter group is a draw-order barrier: everything painted
|
|
// afterwards must sort above the group's end marker so it can't fall back
|
|
// inside the group's order range (subsequent non-overlapping content otherwise
|
|
// reuses a low order). Mirrors the floor raised before deferred draws in
|
|
// `raise_order_floor`.
|
|
self.primitive_bounds.set_order_floor(order + 1);
|
|
}
|
|
self.filter_boundaries.push(boundary.clone());
|
|
}
|
|
}
|
|
self.paint_operations
|
|
.push(PaintOperation::Primitive(primitive));
|
|
}
|
|
|
|
pub fn replay(&mut self, range: Range<usize>, prev_scene: &Scene) {
|
|
for operation in &prev_scene.paint_operations[range] {
|
|
match operation {
|
|
PaintOperation::Primitive(primitive) => self.insert_primitive(primitive.clone()),
|
|
PaintOperation::StartLayer(bounds) => self.push_layer(*bounds),
|
|
PaintOperation::EndLayer => self.pop_layer(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn finish(&mut self) {
|
|
self.shadows.sort_by_key(|shadow| shadow.order);
|
|
self.quads.sort_by_key(|quad| quad.order);
|
|
self.paths.sort_by_key(|path| path.order);
|
|
self.underlines.sort_by_key(|underline| underline.order);
|
|
self.monochrome_sprites
|
|
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
|
|
self.subpixel_sprites
|
|
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
|
|
self.polychrome_sprites
|
|
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
|
|
self.surfaces.sort_by_key(|surface| surface.order);
|
|
self.backdrop_filters.sort_by_key(|filter| filter.order);
|
|
// Markers normally get distinct, monotonically-increasing orders (children overlap
|
|
// their group bounds and so sort strictly between the start and end). The `!is_start`
|
|
// tiebreak only matters for a degenerate empty group whose start and end tie: it keeps
|
|
// the start (false = 0) ahead of the end (true = 1) so the pair stays well-formed.
|
|
self.filter_boundaries
|
|
.sort_by_key(|boundary| (boundary.order, !boundary.is_start));
|
|
}
|
|
|
|
#[cfg_attr(
|
|
all(
|
|
any(target_os = "linux", target_os = "freebsd"),
|
|
not(any(feature = "x11", feature = "wayland"))
|
|
),
|
|
allow(dead_code)
|
|
)]
|
|
pub fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> + '_ {
|
|
BatchIterator {
|
|
shadows_start: 0,
|
|
shadows_iter: self.shadows.iter().peekable(),
|
|
quads_start: 0,
|
|
quads_iter: self.quads.iter().peekable(),
|
|
paths_start: 0,
|
|
paths_iter: self.paths.iter().peekable(),
|
|
underlines_start: 0,
|
|
underlines_iter: self.underlines.iter().peekable(),
|
|
monochrome_sprites_start: 0,
|
|
monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(),
|
|
subpixel_sprites_start: 0,
|
|
subpixel_sprites_iter: self.subpixel_sprites.iter().peekable(),
|
|
polychrome_sprites_start: 0,
|
|
polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(),
|
|
surfaces_start: 0,
|
|
surfaces_iter: self.surfaces.iter().peekable(),
|
|
backdrop_filters_start: 0,
|
|
backdrop_filters_iter: self.backdrop_filters.iter().peekable(),
|
|
filter_boundaries_start: 0,
|
|
filter_boundaries_iter: self.filter_boundaries.iter().peekable(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Default)]
|
|
#[cfg_attr(
|
|
all(
|
|
any(target_os = "linux", target_os = "freebsd"),
|
|
not(any(feature = "x11", feature = "wayland"))
|
|
),
|
|
allow(dead_code)
|
|
)]
|
|
pub(crate) enum PrimitiveKind {
|
|
// Lowest discriminant: at an equal order, a content-filter group-start is emitted before
|
|
// the group's own content so the renderer redirects rendering before any child draws.
|
|
FilterBoundaryStart,
|
|
Shadow,
|
|
#[default]
|
|
Quad,
|
|
Path,
|
|
Underline,
|
|
MonochromeSprite,
|
|
SubpixelSprite,
|
|
PolychromeSprite,
|
|
Surface,
|
|
BackdropFilter,
|
|
// Highest discriminant: at an equal order, a group-end is emitted after the group's content
|
|
// so the renderer composites the filtered group only once every child has been drawn.
|
|
FilterBoundaryEnd,
|
|
}
|
|
|
|
pub(crate) enum PaintOperation {
|
|
Primitive(Primitive),
|
|
StartLayer(Bounds<ScaledPixels>),
|
|
EndLayer,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
#[expect(missing_docs)]
|
|
pub enum Primitive {
|
|
Shadow(Shadow),
|
|
Quad(Quad),
|
|
Path(Path<ScaledPixels>),
|
|
Underline(Underline),
|
|
MonochromeSprite(MonochromeSprite),
|
|
SubpixelSprite(SubpixelSprite),
|
|
PolychromeSprite(PolychromeSprite),
|
|
Surface(PaintSurface),
|
|
BackdropFilter(BackdropFilter),
|
|
FilterBoundary(FilterBoundary),
|
|
}
|
|
|
|
#[expect(missing_docs)]
|
|
impl Primitive {
|
|
pub fn bounds(&self) -> &Bounds<ScaledPixels> {
|
|
match self {
|
|
Primitive::Shadow(shadow) => &shadow.bounds,
|
|
Primitive::Quad(quad) => &quad.bounds,
|
|
Primitive::Path(path) => &path.bounds,
|
|
Primitive::Underline(underline) => &underline.bounds,
|
|
Primitive::MonochromeSprite(sprite) => &sprite.bounds,
|
|
Primitive::SubpixelSprite(sprite) => &sprite.bounds,
|
|
Primitive::PolychromeSprite(sprite) => &sprite.bounds,
|
|
Primitive::Surface(surface) => &surface.bounds,
|
|
Primitive::BackdropFilter(filter) => &filter.bounds,
|
|
Primitive::FilterBoundary(boundary) => &boundary.bounds,
|
|
}
|
|
}
|
|
|
|
pub fn content_mask(&self) -> &ContentMask<ScaledPixels> {
|
|
match self {
|
|
Primitive::Shadow(shadow) => &shadow.content_mask,
|
|
Primitive::Quad(quad) => &quad.content_mask,
|
|
Primitive::Path(path) => &path.content_mask,
|
|
Primitive::Underline(underline) => &underline.content_mask,
|
|
Primitive::MonochromeSprite(sprite) => &sprite.content_mask,
|
|
Primitive::SubpixelSprite(sprite) => &sprite.content_mask,
|
|
Primitive::PolychromeSprite(sprite) => &sprite.content_mask,
|
|
Primitive::Surface(surface) => &surface.content_mask,
|
|
Primitive::BackdropFilter(filter) => &filter.content_mask,
|
|
Primitive::FilterBoundary(boundary) => &boundary.content_mask,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(
|
|
all(
|
|
any(target_os = "linux", target_os = "freebsd"),
|
|
not(any(feature = "x11", feature = "wayland"))
|
|
),
|
|
allow(dead_code)
|
|
)]
|
|
struct BatchIterator<'a> {
|
|
shadows_start: usize,
|
|
shadows_iter: Peekable<slice::Iter<'a, Shadow>>,
|
|
quads_start: usize,
|
|
quads_iter: Peekable<slice::Iter<'a, Quad>>,
|
|
paths_start: usize,
|
|
paths_iter: Peekable<slice::Iter<'a, Path<ScaledPixels>>>,
|
|
underlines_start: usize,
|
|
underlines_iter: Peekable<slice::Iter<'a, Underline>>,
|
|
monochrome_sprites_start: usize,
|
|
monochrome_sprites_iter: Peekable<slice::Iter<'a, MonochromeSprite>>,
|
|
subpixel_sprites_start: usize,
|
|
subpixel_sprites_iter: Peekable<slice::Iter<'a, SubpixelSprite>>,
|
|
polychrome_sprites_start: usize,
|
|
polychrome_sprites_iter: Peekable<slice::Iter<'a, PolychromeSprite>>,
|
|
surfaces_start: usize,
|
|
surfaces_iter: Peekable<slice::Iter<'a, PaintSurface>>,
|
|
backdrop_filters_start: usize,
|
|
backdrop_filters_iter: Peekable<slice::Iter<'a, BackdropFilter>>,
|
|
filter_boundaries_start: usize,
|
|
filter_boundaries_iter: Peekable<slice::Iter<'a, FilterBoundary>>,
|
|
}
|
|
|
|
impl<'a> Iterator for BatchIterator<'a> {
|
|
type Item = PrimitiveBatch;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
let mut orders_and_kinds = [
|
|
(
|
|
self.shadows_iter.peek().map(|s| s.order),
|
|
PrimitiveKind::Shadow,
|
|
),
|
|
(self.quads_iter.peek().map(|q| q.order), PrimitiveKind::Quad),
|
|
(self.paths_iter.peek().map(|q| q.order), PrimitiveKind::Path),
|
|
(
|
|
self.underlines_iter.peek().map(|u| u.order),
|
|
PrimitiveKind::Underline,
|
|
),
|
|
(
|
|
self.monochrome_sprites_iter.peek().map(|s| s.order),
|
|
PrimitiveKind::MonochromeSprite,
|
|
),
|
|
(
|
|
self.subpixel_sprites_iter.peek().map(|s| s.order),
|
|
PrimitiveKind::SubpixelSprite,
|
|
),
|
|
(
|
|
self.polychrome_sprites_iter.peek().map(|s| s.order),
|
|
PrimitiveKind::PolychromeSprite,
|
|
),
|
|
(
|
|
self.surfaces_iter.peek().map(|s| s.order),
|
|
PrimitiveKind::Surface,
|
|
),
|
|
(
|
|
self.backdrop_filters_iter.peek().map(|f| f.order),
|
|
PrimitiveKind::BackdropFilter,
|
|
),
|
|
(
|
|
self.filter_boundaries_iter.peek().map(|b| b.order),
|
|
// The same vec yields both start and end markers; the discriminant decides
|
|
// where the next marker sorts relative to draw batches at an equal order
|
|
// (start before content, end after).
|
|
match self.filter_boundaries_iter.peek() {
|
|
Some(boundary) if boundary.is_start => PrimitiveKind::FilterBoundaryStart,
|
|
_ => PrimitiveKind::FilterBoundaryEnd,
|
|
},
|
|
),
|
|
];
|
|
orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind));
|
|
|
|
let first = orders_and_kinds[0];
|
|
let second = orders_and_kinds[1];
|
|
let (batch_kind, max_order_and_kind) = if first.0.is_some() {
|
|
(first.1, (second.0.unwrap_or(u32::MAX), second.1))
|
|
} else {
|
|
return None;
|
|
};
|
|
|
|
match batch_kind {
|
|
PrimitiveKind::Shadow => {
|
|
let shadows_start = self.shadows_start;
|
|
let mut shadows_end = shadows_start + 1;
|
|
self.shadows_iter.next();
|
|
while self
|
|
.shadows_iter
|
|
.next_if(|shadow| (shadow.order, batch_kind) < max_order_and_kind)
|
|
.is_some()
|
|
{
|
|
shadows_end += 1;
|
|
}
|
|
self.shadows_start = shadows_end;
|
|
Some(PrimitiveBatch::Shadows(shadows_start..shadows_end))
|
|
}
|
|
PrimitiveKind::Quad => {
|
|
let quads_start = self.quads_start;
|
|
let mut quads_end = quads_start + 1;
|
|
self.quads_iter.next();
|
|
while self
|
|
.quads_iter
|
|
.next_if(|quad| (quad.order, batch_kind) < max_order_and_kind)
|
|
.is_some()
|
|
{
|
|
quads_end += 1;
|
|
}
|
|
self.quads_start = quads_end;
|
|
Some(PrimitiveBatch::Quads(quads_start..quads_end))
|
|
}
|
|
PrimitiveKind::Path => {
|
|
let paths_start = self.paths_start;
|
|
let mut paths_end = paths_start + 1;
|
|
self.paths_iter.next();
|
|
while self
|
|
.paths_iter
|
|
.next_if(|path| (path.order, batch_kind) < max_order_and_kind)
|
|
.is_some()
|
|
{
|
|
paths_end += 1;
|
|
}
|
|
self.paths_start = paths_end;
|
|
Some(PrimitiveBatch::Paths(paths_start..paths_end))
|
|
}
|
|
PrimitiveKind::Underline => {
|
|
let underlines_start = self.underlines_start;
|
|
let mut underlines_end = underlines_start + 1;
|
|
self.underlines_iter.next();
|
|
while self
|
|
.underlines_iter
|
|
.next_if(|underline| (underline.order, batch_kind) < max_order_and_kind)
|
|
.is_some()
|
|
{
|
|
underlines_end += 1;
|
|
}
|
|
self.underlines_start = underlines_end;
|
|
Some(PrimitiveBatch::Underlines(underlines_start..underlines_end))
|
|
}
|
|
PrimitiveKind::MonochromeSprite => {
|
|
let texture_id = self.monochrome_sprites_iter.peek().unwrap().tile.texture_id;
|
|
let sprites_start = self.monochrome_sprites_start;
|
|
let mut sprites_end = sprites_start + 1;
|
|
self.monochrome_sprites_iter.next();
|
|
while self
|
|
.monochrome_sprites_iter
|
|
.next_if(|sprite| {
|
|
(sprite.order, batch_kind) < max_order_and_kind
|
|
&& sprite.tile.texture_id == texture_id
|
|
})
|
|
.is_some()
|
|
{
|
|
sprites_end += 1;
|
|
}
|
|
self.monochrome_sprites_start = sprites_end;
|
|
Some(PrimitiveBatch::MonochromeSprites {
|
|
texture_id,
|
|
range: sprites_start..sprites_end,
|
|
})
|
|
}
|
|
PrimitiveKind::SubpixelSprite => {
|
|
let texture_id = self.subpixel_sprites_iter.peek().unwrap().tile.texture_id;
|
|
let sprites_start = self.subpixel_sprites_start;
|
|
let mut sprites_end = sprites_start + 1;
|
|
self.subpixel_sprites_iter.next();
|
|
while self
|
|
.subpixel_sprites_iter
|
|
.next_if(|sprite| {
|
|
(sprite.order, batch_kind) < max_order_and_kind
|
|
&& sprite.tile.texture_id == texture_id
|
|
})
|
|
.is_some()
|
|
{
|
|
sprites_end += 1;
|
|
}
|
|
self.subpixel_sprites_start = sprites_end;
|
|
Some(PrimitiveBatch::SubpixelSprites {
|
|
texture_id,
|
|
range: sprites_start..sprites_end,
|
|
})
|
|
}
|
|
PrimitiveKind::PolychromeSprite => {
|
|
let texture_id = self.polychrome_sprites_iter.peek().unwrap().tile.texture_id;
|
|
let sprites_start = self.polychrome_sprites_start;
|
|
let mut sprites_end = sprites_start + 1;
|
|
self.polychrome_sprites_iter.next();
|
|
while self
|
|
.polychrome_sprites_iter
|
|
.next_if(|sprite| {
|
|
(sprite.order, batch_kind) < max_order_and_kind
|
|
&& sprite.tile.texture_id == texture_id
|
|
})
|
|
.is_some()
|
|
{
|
|
sprites_end += 1;
|
|
}
|
|
self.polychrome_sprites_start = sprites_end;
|
|
Some(PrimitiveBatch::PolychromeSprites {
|
|
texture_id,
|
|
range: sprites_start..sprites_end,
|
|
})
|
|
}
|
|
PrimitiveKind::Surface => {
|
|
let surfaces_start = self.surfaces_start;
|
|
let mut surfaces_end = surfaces_start + 1;
|
|
self.surfaces_iter.next();
|
|
while self
|
|
.surfaces_iter
|
|
.next_if(|surface| (surface.order, batch_kind) < max_order_and_kind)
|
|
.is_some()
|
|
{
|
|
surfaces_end += 1;
|
|
}
|
|
self.surfaces_start = surfaces_end;
|
|
Some(PrimitiveBatch::Surfaces(surfaces_start..surfaces_end))
|
|
}
|
|
PrimitiveKind::BackdropFilter => {
|
|
let backdrop_filters_start = self.backdrop_filters_start;
|
|
let mut backdrop_filters_end = backdrop_filters_start + 1;
|
|
self.backdrop_filters_iter.next();
|
|
while self
|
|
.backdrop_filters_iter
|
|
.next_if(|filter| (filter.order, batch_kind) < max_order_and_kind)
|
|
.is_some()
|
|
{
|
|
backdrop_filters_end += 1;
|
|
}
|
|
self.backdrop_filters_start = backdrop_filters_end;
|
|
Some(PrimitiveBatch::BackdropFilters(
|
|
backdrop_filters_start..backdrop_filters_end,
|
|
))
|
|
}
|
|
// Boundaries are emitted one at a time (never merged) so the renderer can switch
|
|
// render targets at exactly the right point in the batch stream.
|
|
PrimitiveKind::FilterBoundaryStart | PrimitiveKind::FilterBoundaryEnd => {
|
|
let index = self.filter_boundaries_start;
|
|
self.filter_boundaries_iter.next();
|
|
self.filter_boundaries_start = index + 1;
|
|
Some(PrimitiveBatch::FilterBoundary(index))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
#[cfg_attr(
|
|
all(
|
|
any(target_os = "linux", target_os = "freebsd"),
|
|
not(any(feature = "x11", feature = "wayland"))
|
|
),
|
|
allow(dead_code)
|
|
)]
|
|
#[allow(missing_docs)]
|
|
pub enum PrimitiveBatch {
|
|
Shadows(Range<usize>),
|
|
Quads(Range<usize>),
|
|
Paths(Range<usize>),
|
|
Underlines(Range<usize>),
|
|
MonochromeSprites {
|
|
texture_id: AtlasTextureId,
|
|
range: Range<usize>,
|
|
},
|
|
#[cfg_attr(target_os = "macos", allow(dead_code))]
|
|
SubpixelSprites {
|
|
texture_id: AtlasTextureId,
|
|
range: Range<usize>,
|
|
},
|
|
PolychromeSprites {
|
|
texture_id: AtlasTextureId,
|
|
range: Range<usize>,
|
|
},
|
|
Surfaces(Range<usize>),
|
|
BackdropFilters(Range<usize>),
|
|
/// A single content-filter group boundary; index into [`Scene::filter_boundaries`]. Read
|
|
/// `is_start` to tell whether this opens the group (switch render target) or closes it
|
|
/// (filter the offscreen target and composite it back).
|
|
FilterBoundary(usize),
|
|
}
|
|
|
|
#[derive(Default, Debug, Copy, Clone)]
|
|
#[repr(C)]
|
|
#[expect(missing_docs)]
|
|
pub struct Quad {
|
|
pub order: DrawOrder,
|
|
pub border_style: BorderStyle,
|
|
pub bounds: Bounds<ScaledPixels>,
|
|
pub content_mask: ContentMask<ScaledPixels>,
|
|
pub background: Background,
|
|
pub border_color: Hsla,
|
|
pub corner_radii: Corners<ScaledPixels>,
|
|
pub border_widths: Edges<ScaledPixels>,
|
|
}
|
|
|
|
impl From<Quad> for Primitive {
|
|
fn from(quad: Quad) -> Self {
|
|
Primitive::Quad(quad)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
#[repr(C)]
|
|
#[expect(missing_docs)]
|
|
pub struct Underline {
|
|
pub order: DrawOrder,
|
|
pub pad: u32, // align to 8 bytes
|
|
pub bounds: Bounds<ScaledPixels>,
|
|
pub content_mask: ContentMask<ScaledPixels>,
|
|
pub color: Hsla,
|
|
pub thickness: ScaledPixels,
|
|
pub wavy: u32,
|
|
}
|
|
|
|
impl From<Underline> for Primitive {
|
|
fn from(underline: Underline) -> Self {
|
|
Primitive::Underline(underline)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
#[repr(C)]
|
|
#[expect(missing_docs)]
|
|
pub struct Shadow {
|
|
pub order: DrawOrder,
|
|
pub blur_radius: ScaledPixels,
|
|
pub bounds: Bounds<ScaledPixels>,
|
|
pub corner_radii: Corners<ScaledPixels>,
|
|
pub content_mask: ContentMask<ScaledPixels>,
|
|
pub color: Hsla,
|
|
pub element_bounds: Bounds<ScaledPixels>,
|
|
pub element_corner_radii: Corners<ScaledPixels>,
|
|
/// 0 = drop shadow (rendered outside the element), 1 = inset shadow (rendered inside).
|
|
pub inset: u32,
|
|
pub pad: u32, // align to 8 bytes
|
|
}
|
|
|
|
impl From<Shadow> for Primitive {
|
|
fn from(shadow: Shadow) -> Self {
|
|
Primitive::Shadow(shadow)
|
|
}
|
|
}
|
|
|
|
/// A backdrop filter blurs (and may otherwise filter) the content already rendered behind
|
|
/// `bounds`, compositing the result into a rounded rectangle — the frosted-glass effect.
|
|
/// Emitted by [`crate::Window::paint_backdrop_filter`]; produces the CSS `backdrop-filter` effect.
|
|
#[derive(Default, Debug, Clone)]
|
|
#[expect(missing_docs)]
|
|
pub struct BackdropFilter {
|
|
pub order: DrawOrder,
|
|
pub bounds: Bounds<ScaledPixels>,
|
|
pub content_mask: ContentMask<ScaledPixels>,
|
|
pub corner_radii: Corners<ScaledPixels>,
|
|
/// The filter chain applied to the backdrop, in scene (device-pixel) space. Identity filters
|
|
/// are dropped at paint time, so a `BackdropFilter` is only emitted when this is non-empty.
|
|
///
|
|
/// Inline capacity is 4: a `SmallVec<[ScaledFilter; 4]>` is the same size as capacity 1 here
|
|
/// (the heap repr already occupies that space), so chains up to 4 filters avoid allocating
|
|
/// at no extra struct size.
|
|
pub filters: SmallVec<[ScaledFilter; 4]>,
|
|
/// Element opacity captured at paint time, multiplied into the composited result.
|
|
pub opacity: f32,
|
|
}
|
|
|
|
impl From<BackdropFilter> for Primitive {
|
|
fn from(filter: BackdropFilter) -> Self {
|
|
Primitive::BackdropFilter(filter)
|
|
}
|
|
}
|
|
|
|
/// The start or end marker of a content-filter (`filter`) isolation group. The element's
|
|
/// subtree is painted between a matched start/end pair; the renderer redirects that span into
|
|
/// an offscreen target, filters it, and composites it back at `bounds`. Produces the CSS
|
|
/// `filter` effect (e.g. blurring the element and its children as a single group).
|
|
#[derive(Debug, Clone)]
|
|
#[expect(missing_docs)]
|
|
pub struct FilterBoundary {
|
|
pub order: DrawOrder,
|
|
pub bounds: Bounds<ScaledPixels>,
|
|
pub content_mask: ContentMask<ScaledPixels>,
|
|
pub corner_radii: Corners<ScaledPixels>,
|
|
/// The filter chain applied to the isolated group, in scene (device-pixel) space. Identity
|
|
/// filters are dropped at paint time, so a `FilterBoundary` is only emitted when non-empty.
|
|
/// Inline capacity 4 (same struct size as 1 here — see [`BackdropFilter::filters`]).
|
|
pub filters: SmallVec<[ScaledFilter; 4]>,
|
|
pub opacity: f32,
|
|
/// `true` for the start marker (opens the group), `false` for the end marker (closes it).
|
|
pub is_start: bool,
|
|
}
|
|
|
|
impl From<FilterBoundary> for Primitive {
|
|
fn from(boundary: FilterBoundary) -> Self {
|
|
Primitive::FilterBoundary(boundary)
|
|
}
|
|
}
|
|
|
|
/// The style of a border.
|
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
|
#[repr(C)]
|
|
pub enum BorderStyle {
|
|
/// A solid border.
|
|
#[default]
|
|
Solid = 0,
|
|
/// A dashed border.
|
|
Dashed = 1,
|
|
}
|
|
|
|
/// A data type representing a 2 dimensional transformation that can be applied to an element.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
#[repr(C)]
|
|
pub struct TransformationMatrix {
|
|
/// 2x2 matrix containing rotation and scale,
|
|
/// stored row-major
|
|
pub rotation_scale: [[f32; 2]; 2],
|
|
/// translation vector
|
|
pub translation: [f32; 2],
|
|
}
|
|
|
|
impl Eq for TransformationMatrix {}
|
|
|
|
impl TransformationMatrix {
|
|
/// The unit matrix, has no effect.
|
|
pub fn unit() -> Self {
|
|
Self {
|
|
rotation_scale: [[1.0, 0.0], [0.0, 1.0]],
|
|
translation: [0.0, 0.0],
|
|
}
|
|
}
|
|
|
|
/// Move the origin by a given point
|
|
pub fn translate(mut self, point: Point<ScaledPixels>) -> Self {
|
|
self.compose(Self {
|
|
rotation_scale: [[1.0, 0.0], [0.0, 1.0]],
|
|
translation: [point.x.0, point.y.0],
|
|
})
|
|
}
|
|
|
|
/// Clockwise rotation in radians around the origin
|
|
pub fn rotate(self, angle: Radians) -> Self {
|
|
self.compose(Self {
|
|
rotation_scale: [
|
|
[angle.0.cos(), -angle.0.sin()],
|
|
[angle.0.sin(), angle.0.cos()],
|
|
],
|
|
translation: [0.0, 0.0],
|
|
})
|
|
}
|
|
|
|
/// Scale around the origin
|
|
pub fn scale(self, size: Size<f32>) -> Self {
|
|
self.compose(Self {
|
|
rotation_scale: [[size.width, 0.0], [0.0, size.height]],
|
|
translation: [0.0, 0.0],
|
|
})
|
|
}
|
|
|
|
/// Perform matrix multiplication with another transformation
|
|
/// to produce a new transformation that is the result of
|
|
/// applying both transformations: first, `other`, then `self`.
|
|
#[inline]
|
|
pub fn compose(self, other: TransformationMatrix) -> TransformationMatrix {
|
|
if other == Self::unit() {
|
|
return self;
|
|
}
|
|
// Perform matrix multiplication
|
|
TransformationMatrix {
|
|
rotation_scale: [
|
|
[
|
|
self.rotation_scale[0][0] * other.rotation_scale[0][0]
|
|
+ self.rotation_scale[0][1] * other.rotation_scale[1][0],
|
|
self.rotation_scale[0][0] * other.rotation_scale[0][1]
|
|
+ self.rotation_scale[0][1] * other.rotation_scale[1][1],
|
|
],
|
|
[
|
|
self.rotation_scale[1][0] * other.rotation_scale[0][0]
|
|
+ self.rotation_scale[1][1] * other.rotation_scale[1][0],
|
|
self.rotation_scale[1][0] * other.rotation_scale[0][1]
|
|
+ self.rotation_scale[1][1] * other.rotation_scale[1][1],
|
|
],
|
|
],
|
|
translation: [
|
|
self.translation[0]
|
|
+ self.rotation_scale[0][0] * other.translation[0]
|
|
+ self.rotation_scale[0][1] * other.translation[1],
|
|
self.translation[1]
|
|
+ self.rotation_scale[1][0] * other.translation[0]
|
|
+ self.rotation_scale[1][1] * other.translation[1],
|
|
],
|
|
}
|
|
}
|
|
|
|
/// Apply transformation to a point, mainly useful for debugging
|
|
pub fn apply(&self, point: Point<Pixels>) -> Point<Pixels> {
|
|
let input = [point.x.0, point.y.0];
|
|
let mut output = self.translation;
|
|
for (i, output_cell) in output.iter_mut().enumerate() {
|
|
for (k, input_cell) in input.iter().enumerate() {
|
|
*output_cell += self.rotation_scale[i][k] * *input_cell;
|
|
}
|
|
}
|
|
Point::new(output[0].into(), output[1].into())
|
|
}
|
|
}
|
|
|
|
impl Default for TransformationMatrix {
|
|
fn default() -> Self {
|
|
Self::unit()
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
#[repr(C)]
|
|
#[expect(missing_docs)]
|
|
pub struct MonochromeSprite {
|
|
pub order: DrawOrder,
|
|
pub pad: u32,
|
|
pub bounds: Bounds<ScaledPixels>,
|
|
pub content_mask: ContentMask<ScaledPixels>,
|
|
pub color: Hsla,
|
|
pub tile: AtlasTile,
|
|
pub transformation: TransformationMatrix,
|
|
}
|
|
|
|
impl From<MonochromeSprite> for Primitive {
|
|
fn from(sprite: MonochromeSprite) -> Self {
|
|
Primitive::MonochromeSprite(sprite)
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
#[repr(C)]
|
|
#[expect(missing_docs)]
|
|
pub struct SubpixelSprite {
|
|
pub order: DrawOrder,
|
|
pub pad: u32, // align to 8 bytes
|
|
pub bounds: Bounds<ScaledPixels>,
|
|
pub content_mask: ContentMask<ScaledPixels>,
|
|
pub color: Hsla,
|
|
pub tile: AtlasTile,
|
|
pub transformation: TransformationMatrix,
|
|
}
|
|
|
|
impl From<SubpixelSprite> for Primitive {
|
|
fn from(sprite: SubpixelSprite) -> Self {
|
|
Primitive::SubpixelSprite(sprite)
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
#[repr(C)]
|
|
#[expect(missing_docs)]
|
|
pub struct PolychromeSprite {
|
|
pub order: DrawOrder,
|
|
pub pad: u32,
|
|
pub grayscale: bool,
|
|
pub opacity: f32,
|
|
pub bounds: Bounds<ScaledPixels>,
|
|
pub content_mask: ContentMask<ScaledPixels>,
|
|
pub corner_radii: Corners<ScaledPixels>,
|
|
pub tile: AtlasTile,
|
|
}
|
|
|
|
impl From<PolychromeSprite> for Primitive {
|
|
fn from(sprite: PolychromeSprite) -> Self {
|
|
Primitive::PolychromeSprite(sprite)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
#[allow(missing_docs)]
|
|
pub struct PaintSurface {
|
|
pub order: DrawOrder,
|
|
pub bounds: Bounds<ScaledPixels>,
|
|
pub content_mask: ContentMask<ScaledPixels>,
|
|
#[cfg(target_os = "macos")]
|
|
pub image_buffer: core_video::pixel_buffer::CVPixelBuffer,
|
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
pub texture: std::sync::Arc<dyn std::any::Any + Send + Sync>,
|
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
pub texture_size: Size<crate::DevicePixels>,
|
|
}
|
|
|
|
impl From<PaintSurface> for Primitive {
|
|
fn from(surface: PaintSurface) -> Self {
|
|
Primitive::Surface(surface)
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
|
#[expect(missing_docs)]
|
|
pub struct PathId(pub usize);
|
|
|
|
/// A line made up of a series of vertices and control points.
|
|
#[derive(Clone, Debug)]
|
|
#[expect(missing_docs)]
|
|
pub struct Path<P: Clone + Debug + Default + PartialEq> {
|
|
pub id: PathId,
|
|
pub order: DrawOrder,
|
|
pub bounds: Bounds<P>,
|
|
pub content_mask: ContentMask<P>,
|
|
pub vertices: Vec<PathVertex<P>>,
|
|
pub color: Background,
|
|
start: Point<P>,
|
|
current: Point<P>,
|
|
contour_count: usize,
|
|
}
|
|
|
|
impl Path<Pixels> {
|
|
/// Create a new path with the given starting point.
|
|
pub fn new(start: Point<Pixels>) -> Self {
|
|
Self {
|
|
id: PathId(0),
|
|
order: DrawOrder::default(),
|
|
vertices: Vec::new(),
|
|
start,
|
|
current: start,
|
|
bounds: Bounds {
|
|
origin: start,
|
|
size: Default::default(),
|
|
},
|
|
content_mask: Default::default(),
|
|
color: Default::default(),
|
|
contour_count: 0,
|
|
}
|
|
}
|
|
|
|
/// Scale this path by the given factor.
|
|
pub fn scale(&self, factor: f32) -> Path<ScaledPixels> {
|
|
Path {
|
|
id: self.id,
|
|
order: self.order,
|
|
bounds: self.bounds.scale(factor),
|
|
content_mask: self.content_mask.scale(factor),
|
|
vertices: self
|
|
.vertices
|
|
.iter()
|
|
.map(|vertex| vertex.scale(factor))
|
|
.collect(),
|
|
start: self.start.map(|start| start.scale(factor)),
|
|
current: self.current.scale(factor),
|
|
contour_count: self.contour_count,
|
|
color: self.color,
|
|
}
|
|
}
|
|
|
|
/// Move the start, current point to the given point.
|
|
pub fn move_to(&mut self, to: Point<Pixels>) {
|
|
self.contour_count += 1;
|
|
self.start = to;
|
|
self.current = to;
|
|
}
|
|
|
|
/// Draw a straight line from the current point to the given point.
|
|
pub fn line_to(&mut self, to: Point<Pixels>) {
|
|
self.contour_count += 1;
|
|
if self.contour_count > 1 {
|
|
self.push_triangle(
|
|
(self.start, self.current, to),
|
|
(point(0., 1.), point(0., 1.), point(0., 1.)),
|
|
);
|
|
}
|
|
self.current = to;
|
|
}
|
|
|
|
/// Draw a curve from the current point to the given point, using the given control point.
|
|
pub fn curve_to(&mut self, to: Point<Pixels>, ctrl: Point<Pixels>) {
|
|
self.contour_count += 1;
|
|
if self.contour_count > 1 {
|
|
self.push_triangle(
|
|
(self.start, self.current, to),
|
|
(point(0., 1.), point(0., 1.), point(0., 1.)),
|
|
);
|
|
}
|
|
|
|
self.push_triangle(
|
|
(self.current, ctrl, to),
|
|
(point(0., 0.), point(0.5, 0.), point(1., 1.)),
|
|
);
|
|
self.current = to;
|
|
}
|
|
|
|
/// Push a triangle to the Path.
|
|
pub fn push_triangle(
|
|
&mut self,
|
|
xy: (Point<Pixels>, Point<Pixels>, Point<Pixels>),
|
|
st: (Point<f32>, Point<f32>, Point<f32>),
|
|
) {
|
|
self.bounds = self
|
|
.bounds
|
|
.union(&Bounds {
|
|
origin: xy.0,
|
|
size: Default::default(),
|
|
})
|
|
.union(&Bounds {
|
|
origin: xy.1,
|
|
size: Default::default(),
|
|
})
|
|
.union(&Bounds {
|
|
origin: xy.2,
|
|
size: Default::default(),
|
|
});
|
|
|
|
self.vertices.push(PathVertex {
|
|
xy_position: xy.0,
|
|
st_position: st.0,
|
|
content_mask: Default::default(),
|
|
});
|
|
self.vertices.push(PathVertex {
|
|
xy_position: xy.1,
|
|
st_position: st.1,
|
|
content_mask: Default::default(),
|
|
});
|
|
self.vertices.push(PathVertex {
|
|
xy_position: xy.2,
|
|
st_position: st.2,
|
|
content_mask: Default::default(),
|
|
});
|
|
}
|
|
}
|
|
|
|
impl<T> Path<T>
|
|
where
|
|
T: Clone + Debug + Default + PartialEq + PartialOrd + Add<T, Output = T> + Sub<Output = T>,
|
|
{
|
|
#[allow(unused)]
|
|
#[expect(missing_docs)]
|
|
pub fn clipped_bounds(&self) -> Bounds<T> {
|
|
self.bounds.intersect(&self.content_mask.bounds)
|
|
}
|
|
}
|
|
|
|
impl From<Path<ScaledPixels>> for Primitive {
|
|
fn from(path: Path<ScaledPixels>) -> Self {
|
|
Primitive::Path(path)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
#[repr(C)]
|
|
#[expect(missing_docs)]
|
|
pub struct PathVertex<P: Clone + Debug + Default + PartialEq> {
|
|
pub xy_position: Point<P>,
|
|
pub st_position: Point<f32>,
|
|
pub content_mask: ContentMask<P>,
|
|
}
|
|
|
|
#[expect(missing_docs)]
|
|
impl PathVertex<Pixels> {
|
|
pub fn scale(&self, factor: f32) -> PathVertex<ScaledPixels> {
|
|
PathVertex {
|
|
xy_position: self.xy_position.scale(factor),
|
|
st_position: self.st_position,
|
|
content_mask: self.content_mask.scale(factor),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{Point, Size};
|
|
|
|
fn sp(value: f32) -> ScaledPixels {
|
|
ScaledPixels(value)
|
|
}
|
|
|
|
/// All test primitives cover the same region so the bounds tree assigns strictly
|
|
/// increasing orders in insertion order — making the expected batch order deterministic.
|
|
fn full_bounds() -> Bounds<ScaledPixels> {
|
|
Bounds {
|
|
origin: Point {
|
|
x: sp(0.0),
|
|
y: sp(0.0),
|
|
},
|
|
size: Size {
|
|
width: sp(100.0),
|
|
height: sp(100.0),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn mask() -> ContentMask<ScaledPixels> {
|
|
ContentMask {
|
|
bounds: full_bounds(),
|
|
}
|
|
}
|
|
|
|
fn quad() -> Quad {
|
|
Quad {
|
|
bounds: full_bounds(),
|
|
content_mask: mask(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// A 100x100 quad whose bounds don't overlap `full_bounds()` (used to exercise the
|
|
/// order-reuse path: non-overlapping content reuses low draw-orders).
|
|
fn detached_quad() -> Quad {
|
|
let bounds = Bounds {
|
|
origin: Point {
|
|
x: sp(200.0),
|
|
y: sp(200.0),
|
|
},
|
|
size: Size {
|
|
width: sp(100.0),
|
|
height: sp(100.0),
|
|
},
|
|
};
|
|
Quad {
|
|
bounds,
|
|
content_mask: ContentMask { bounds },
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn boundary(is_start: bool) -> FilterBoundary {
|
|
FilterBoundary {
|
|
order: 0,
|
|
bounds: full_bounds(),
|
|
content_mask: mask(),
|
|
corner_radii: Corners::default(),
|
|
filters: smallvec::smallvec![ScaledFilter::Blur(sp(8.0))],
|
|
opacity: 1.0,
|
|
is_start,
|
|
}
|
|
}
|
|
|
|
fn backdrop() -> BackdropFilter {
|
|
BackdropFilter {
|
|
bounds: full_bounds(),
|
|
content_mask: mask(),
|
|
corner_radii: Corners::default(),
|
|
filters: smallvec::smallvec![ScaledFilter::Blur(sp(20.0))],
|
|
opacity: 1.0,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn batch_kinds(scene: &mut Scene) -> Vec<&'static str> {
|
|
scene.finish();
|
|
scene
|
|
.batches()
|
|
.map(|batch| match batch {
|
|
PrimitiveBatch::Quads(_) => "quad",
|
|
PrimitiveBatch::BackdropFilters(_) => "backdrop",
|
|
PrimitiveBatch::FilterBoundary(ix) => {
|
|
if scene.filter_boundaries[ix].is_start {
|
|
"start"
|
|
} else {
|
|
"end"
|
|
}
|
|
}
|
|
_ => "other",
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn content_filter_group_brackets_its_children() {
|
|
let mut scene = Scene::default();
|
|
// Background painted before the filtered element.
|
|
scene.insert_primitive(quad());
|
|
// A content-filtered element: start marker, its child, end marker.
|
|
scene.insert_primitive(boundary(true));
|
|
scene.insert_primitive(quad());
|
|
scene.insert_primitive(boundary(false));
|
|
|
|
// The start must precede the group's child and the end must follow it, so the
|
|
// renderer can redirect rendering for exactly the group's span.
|
|
assert_eq!(
|
|
batch_kinds(&mut scene),
|
|
vec!["quad", "start", "quad", "end"]
|
|
);
|
|
}
|
|
|
|
// Note: this validates only the *scene ordering* of nested filter boundaries (start/child/
|
|
// end interleaving), not that a renderer actually isolates both levels — that depends on the
|
|
// backend's group-texture pool (see MAX_FILTER_DEPTH) and is exercised by the `blur` example.
|
|
#[test]
|
|
fn nested_content_filters_emit_well_nested_ordering() {
|
|
let mut scene = Scene::default();
|
|
scene.insert_primitive(boundary(true)); // outer start
|
|
scene.insert_primitive(quad()); // outer child
|
|
scene.insert_primitive(boundary(true)); // inner start
|
|
scene.insert_primitive(quad()); // inner child
|
|
scene.insert_primitive(boundary(false)); // inner end
|
|
scene.insert_primitive(boundary(false)); // outer end
|
|
|
|
assert_eq!(
|
|
batch_kinds(&mut scene),
|
|
vec!["start", "quad", "start", "quad", "end", "end"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn content_after_a_filter_group_sorts_above_it() {
|
|
let mut scene = Scene::default();
|
|
// A content-filtered element: start marker, its child, end marker.
|
|
scene.insert_primitive(boundary(true));
|
|
scene.insert_primitive(quad());
|
|
scene.insert_primitive(boundary(false));
|
|
// A sibling painted after the group that does NOT overlap it. Without the close-time
|
|
// order-floor it would reuse the lowest order, tie with the start marker, and be swept
|
|
// into the group (start, quad, quad, end); it must instead sort after the end marker.
|
|
scene.insert_primitive(detached_quad());
|
|
|
|
assert_eq!(
|
|
batch_kinds(&mut scene),
|
|
vec!["start", "quad", "end", "quad"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn backdrop_filter_sorts_before_a_later_overlapping_quad() {
|
|
let mut scene = Scene::default();
|
|
// Content behind the frosted panel.
|
|
scene.insert_primitive(quad());
|
|
// The panel: its backdrop snapshot, then its (translucent) background quad on top.
|
|
scene.insert_primitive(backdrop());
|
|
scene.insert_primitive(quad());
|
|
|
|
assert_eq!(batch_kinds(&mut scene), vec!["quad", "backdrop", "quad"]);
|
|
}
|
|
}
|