teksilo_canvas/render_frame.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4use std::borrow::Cow;
5
6use crate::geometry::{Rect, Transform2D};
7use crate::paint::{FillRule, StrokeSpace, StrokeStyle};
8
9/// The complete render output for one frame. This is the boundary between
10/// platform-independent widget code and GPU-specific rendering code.
11#[derive(Debug, Clone, Default)]
12pub struct RenderFrame {
13 pub glyphs: Vec<GlyphQuad>,
14 pub images: Vec<ImageQuad>,
15 pub decorations: Vec<DecorationRect>,
16 /// Transform-invariant ("cosmetic" / hairline) lines. Unlike
17 /// `decorations`, the width here is NOT baked into geometry — the
18 /// renderer applies a constant device-pixel thickness regardless of the
19 /// active transform. See [`DrawCommand::CosmeticLine`].
20 pub cosmetic_lines: Vec<CosmeticLine>,
21 pub shapes: Vec<ShapeQuad>,
22 pub shadows: Vec<ShadowQuad>,
23 pub rasterized: Vec<RasterizedQuad>,
24 pub paths: Vec<PathEntry>,
25 /// Animated quads (procedural or sprite-atlas kinds). Emitted by
26 /// widgets that opt into the shader-driven animation pipeline via
27 /// `ctx.animated_quad()`. The fragment shader samples per-slot
28 /// state from a renderer-side uniform buffer updated each frame by
29 /// the widget tree — the widget's own `paint()` runs only when
30 /// layout changes, not once per animation frame.
31 pub animated_quads: Vec<AnimatedQuadDraw>,
32 /// Per-slot `AnimParams`, indexed by the `slot` field of each
33 /// `AnimatedQuadDraw`. Recomputed by the widget tree every frame
34 /// (phase advanced, colors resolved against the current theme)
35 /// and uploaded to the renderer's uniform buffer at the top of
36 /// `Renderer::render`. Slots whose widget is dormant / offscreen /
37 /// in an inactive window keep their last-written values — the
38 /// fragment shader still renders, just with stale phase for one
39 /// frame until the next tick resumes.
40 pub anim_params: Vec<AnimParams>,
41 pub draw_order: Vec<DrawCommand>,
42 /// Images that need GPU registration before rendering this frame.
43 pub pending_images: Vec<PendingImage>,
44 /// Opaque [`TextLayout::layout_key`](crate::text_backend::TextLayout::layout_key)
45 /// values for every `draw_text*` call that produced glyphs in this
46 /// frame. When a widget's `cached_paint` is reused without re-running
47 /// `paint()`, the renderer calls `TextBackend::touch_layout(key)` for
48 /// each stored key so the backend can refresh the underlying glyph
49 /// cache timestamps and avoid evicting still-visible glyphs.
50 pub layout_keys: Vec<u64>,
51}
52
53impl RenderFrame {
54 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn is_empty(&self) -> bool {
59 self.draw_order.is_empty()
60 }
61
62 /// Merge another frame into this one, appending all entries
63 /// and adjusting draw_order indices.
64 pub fn merge(&mut self, other: &RenderFrame) {
65 let glyph_offset = self.glyphs.len();
66 let image_offset = self.images.len();
67 let decoration_offset = self.decorations.len();
68 let cosmetic_line_offset = self.cosmetic_lines.len();
69 let shape_offset = self.shapes.len();
70 let shadow_offset = self.shadows.len();
71 let rasterized_offset = self.rasterized.len();
72 let path_offset = self.paths.len();
73 let animated_offset = self.animated_quads.len();
74
75 self.glyphs.extend_from_slice(&other.glyphs);
76 self.images.extend_from_slice(&other.images);
77 self.decorations.extend_from_slice(&other.decorations);
78 self.cosmetic_lines.extend_from_slice(&other.cosmetic_lines);
79 self.shapes.extend_from_slice(&other.shapes);
80 self.shadows.extend_from_slice(&other.shadows);
81 self.rasterized.extend_from_slice(&other.rasterized);
82 self.paths.extend_from_slice(&other.paths);
83 self.animated_quads.extend_from_slice(&other.animated_quads);
84 // `anim_params` is NOT merged index-wise — the widget tree
85 // writes one authoritative slice per frame (indexed by
86 // registry slot, which is global across the tree). Cached
87 // sub-frames carry empty `anim_params`; the outer tree
88 // replaces it wholesale after `render()` is called.
89 self.layout_keys.extend_from_slice(&other.layout_keys);
90 // Merge pending image registrations (deduped by renderer)
91 for pending in &other.pending_images {
92 if !self.pending_images.iter().any(|p| p.name == pending.name) {
93 self.pending_images.push(pending.clone());
94 }
95 }
96
97 for cmd in &other.draw_order {
98 let shifted = match cmd {
99 DrawCommand::Glyph(i) => DrawCommand::Glyph(i + glyph_offset),
100 DrawCommand::Image(i) => DrawCommand::Image(i + image_offset),
101 DrawCommand::Decoration(i) => DrawCommand::Decoration(i + decoration_offset),
102 DrawCommand::CosmeticLine(i) => DrawCommand::CosmeticLine(i + cosmetic_line_offset),
103 DrawCommand::Shape(i) => DrawCommand::Shape(i + shape_offset),
104 DrawCommand::Shadow(i) => DrawCommand::Shadow(i + shadow_offset),
105 DrawCommand::Rasterized(i) => DrawCommand::Rasterized(i + rasterized_offset),
106 DrawCommand::Path(i) => DrawCommand::Path(i + path_offset),
107 DrawCommand::AnimatedQuad(i) => DrawCommand::AnimatedQuad(i + animated_offset),
108 other => other.clone(),
109 };
110 self.draw_order.push(shifted);
111 }
112 }
113}
114
115impl RenderFrame {
116 /// Validate that clip and opacity stacks are balanced in the draw order.
117 /// Only runs in debug builds. Panics with a descriptive message if
118 /// any push/pop pair is unbalanced.
119 pub fn debug_validate_stacks(&self) {
120 if !cfg!(debug_assertions) {
121 return;
122 }
123 let mut clip_depth: i32 = 0;
124 let mut opacity_depth: i32 = 0;
125 let mut blend_depth: i32 = 0;
126 let mut transform_depth: i32 = 0;
127 let mut blur_depth: i32 = 0;
128 for (i, cmd) in self.draw_order.iter().enumerate() {
129 match cmd {
130 DrawCommand::SetClip(_) => clip_depth += 1,
131 DrawCommand::ClearClip => {
132 clip_depth -= 1;
133 debug_assert!(
134 clip_depth >= 0,
135 "RenderFrame: ClearClip without matching SetClip at draw_order[{i}]"
136 );
137 }
138 DrawCommand::SetOpacity(_) => opacity_depth += 1,
139 DrawCommand::RestoreOpacity => {
140 opacity_depth -= 1;
141 debug_assert!(
142 opacity_depth >= 0,
143 "RenderFrame: RestoreOpacity without matching SetOpacity at draw_order[{i}]"
144 );
145 }
146 DrawCommand::SetBlendMode(_) => blend_depth += 1,
147 DrawCommand::RestoreBlendMode => {
148 blend_depth -= 1;
149 debug_assert!(
150 blend_depth >= 0,
151 "RenderFrame: RestoreBlendMode without matching SetBlendMode at draw_order[{i}]"
152 );
153 }
154 DrawCommand::PushTransform(_) => transform_depth += 1,
155 DrawCommand::PopTransform => {
156 transform_depth -= 1;
157 debug_assert!(
158 transform_depth >= 0,
159 "RenderFrame: PopTransform without matching PushTransform at draw_order[{i}]"
160 );
161 }
162 DrawCommand::BeginBlurredSubtree { .. } => blur_depth += 1,
163 DrawCommand::EndBlurredSubtree => {
164 blur_depth -= 1;
165 debug_assert!(
166 blur_depth >= 0,
167 "RenderFrame: EndBlurredSubtree without matching BeginBlurredSubtree at draw_order[{i}]"
168 );
169 }
170 _ => {}
171 }
172 }
173 debug_assert!(
174 clip_depth == 0,
175 "RenderFrame: {clip_depth} unmatched SetClip(s) without ClearClip"
176 );
177 debug_assert!(
178 opacity_depth == 0,
179 "RenderFrame: {opacity_depth} unmatched SetOpacity(s) without RestoreOpacity"
180 );
181 debug_assert!(
182 blend_depth == 0,
183 "RenderFrame: {blend_depth} unmatched SetBlendMode(s) without RestoreBlendMode"
184 );
185 debug_assert!(
186 transform_depth == 0,
187 "RenderFrame: {transform_depth} unmatched PushTransform(s) without PopTransform"
188 );
189 debug_assert!(
190 blur_depth == 0,
191 "RenderFrame: {blur_depth} unmatched BeginBlurredSubtree(s) without EndBlurredSubtree"
192 );
193 }
194}
195
196/// A positioned glyph to render as a textured rectangle from the glyph atlas.
197#[derive(Debug, Clone, Copy, PartialEq)]
198pub struct GlyphQuad {
199 /// Screen position and size: [x, y, width, height] in logical pixels.
200 pub screen: [f32; 4],
201 /// Atlas position and size: [x, y, width, height] in atlas texture coordinates.
202 pub atlas: [f32; 4],
203 /// Glyph color: [r, g, b, a]. For monochrome glyphs this is the text
204 /// tint. For color emoji it is `[1, 1, 1, 1]` — the atlas region
205 /// already holds the pre-multiplied RGBA bitmap, so the renderer
206 /// samples `texture.rgb` directly.
207 pub color: [f32; 4],
208 /// `true` if the atlas region holds a pre-multiplied RGBA color
209 /// bitmap (color emoji via COLR/CBDT/sbix). When set, the renderer
210 /// must sample `texture.rgb` instead of using the texture as an
211 /// alpha mask.
212 pub is_color: bool,
213}
214
215/// An image quad to render as a textured rectangle.
216#[derive(Debug, Clone, PartialEq)]
217pub struct ImageQuad {
218 /// Screen position and size: [x, y, width, height] in logical pixels.
219 pub screen: [f32; 4],
220 /// Resource name of the image.
221 pub name: String,
222 /// When `Some(color)`, the image is rendered as an alpha mask tinted
223 /// with this color (shader flag=0). When `None`, the image is rendered
224 /// in full color (shader flag=1, existing behavior).
225 pub tint: Option<[f32; 4]>,
226}
227
228/// An image that needs to be registered (uploaded to GPU) before rendering.
229/// Widgets emit these during paint for embedded raster resources.
230#[derive(Debug, Clone)]
231pub struct PendingImage {
232 /// Resource name to register under.
233 pub name: String,
234 /// Image width in pixels.
235 pub width: u32,
236 /// Image height in pixels.
237 pub height: u32,
238 /// RGBA pixel data. Uses `Cow` for zero-copy with compile-time data.
239 pub pixels: Cow<'static, [u8]>,
240}
241
242/// A colored rectangle for decorations (selections, cursors, underlines, borders, etc.).
243#[derive(Debug, Clone, Copy, PartialEq)]
244pub struct DecorationRect {
245 /// Position and size: [x, y, width, height] in logical pixels.
246 pub rect: [f32; 4],
247 /// Color: [r, g, b, a].
248 pub color: [f32; 4],
249 /// What kind of decoration this is.
250 pub kind: DecorationKind,
251}
252
253/// The kind of decoration a DecorationRect represents.
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum DecorationKind {
256 WidgetBackground,
257 Selection,
258 Cursor,
259 Underline,
260 Overline,
261 Strikeout,
262 FocusRing,
263 DropIndicator,
264 TableBorder,
265 TableCellBackground,
266 BlockBackground,
267 TextBackground,
268 CellSelection,
269}
270
271/// A transform-invariant ("cosmetic" / hairline) line. The endpoints are in
272/// logical pixels and follow the active transform; `width` is in logical
273/// pixels but applied as a constant device thickness (× scale_factor), NOT
274/// scaled by the transform's zoom. Emitted by
275/// [`Canvas::draw_line`](crate::Canvas::draw_line) /
276/// [`Canvas::stroke_rect`](crate::Canvas::stroke_rect) when the stroke is
277/// [`StrokeSpace::Device`].
278#[derive(Debug, Clone, Copy, PartialEq)]
279pub struct CosmeticLine {
280 /// Start point [x, y] in logical pixels.
281 pub from: [f32; 2],
282 /// End point [x, y] in logical pixels.
283 pub to: [f32; 2],
284 /// Stroke thickness in logical pixels, held transform-invariant.
285 pub width: f32,
286 /// Color: [r, g, b, a].
287 pub color: [f32; 4],
288}
289
290/// A shape rendered via SDF (signed distance field) shaders.
291#[derive(Debug, Clone, PartialEq)]
292pub struct ShapeQuad {
293 /// Screen position and size: [x, y, width, height] in logical pixels.
294 pub screen: [f32; 4],
295 /// Fill color: [r, g, b, a].
296 pub color: [f32; 4],
297 /// What shape to render.
298 pub shape: ShapeKind,
299 /// Stroke width (0.0 for filled shapes).
300 pub stroke_width: f32,
301 /// Whether the stroke width is logical (scales with the view transform) or
302 /// device/cosmetic (held constant in device pixels, invariant to zoom). A
303 /// cosmetic stroke keeps a hairline border crisp at any scene zoom. Fills
304 /// (`stroke_width == 0.0`) ignore this.
305 pub stroke_space: StrokeSpace,
306 /// Corner radii: [top_left, top_right, bottom_right, bottom_left].
307 pub corner_radii: [f32; 4],
308 /// Paint type for the shape.
309 pub paint_data: PaintData,
310}
311
312/// The kind of SDF shape.
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum ShapeKind {
315 RoundedRect,
316 Circle,
317 Ellipse,
318}
319
320/// A CPU-rasterized path result, stored in the shape atlas.
321#[derive(Debug, Clone, Copy, PartialEq)]
322pub struct RasterizedQuad {
323 /// Screen position and size: [x, y, width, height] in logical pixels.
324 pub screen: [f32; 4],
325 /// Shape atlas position and size: [x, y, width, height] in atlas coordinates.
326 pub atlas: [f32; 4],
327 /// Tint color: [r, g, b, a].
328 pub color: [f32; 4],
329}
330
331/// A shadow rendered behind a shape using a separate GPU pipeline with Gaussian blur.
332#[derive(Debug, Clone, PartialEq)]
333pub struct ShadowQuad {
334 /// Shadow bounding box (expanded by blur + spread + offset): [x, y, width, height].
335 pub screen: [f32; 4],
336 /// Shadow color: [r, g, b, a].
337 pub color: [f32; 4],
338 /// Corner radii matching the shape: [top_left, top_right, bottom_right, bottom_left].
339 pub corner_radii: [f32; 4],
340 /// The original shape rect (before offset/spread): [x, y, width, height].
341 pub shape_rect: [f32; 4],
342 /// Gaussian blur radius.
343 pub blur_radius: f32,
344 /// Shadow spread amount.
345 pub spread: f32,
346}
347
348/// A path to be rasterized on the CPU (Tier 3). Stored in the RenderFrame
349/// until the renderer rasterizes it into the shape atlas and converts it
350/// to a [`RasterizedQuad`].
351#[derive(Debug, Clone, PartialEq)]
352pub struct PathEntry {
353 /// The path commands to rasterize.
354 pub path: crate::path::Path,
355 /// Fill color: [r, g, b, a].
356 pub color: [f32; 4],
357 /// Stroke style (width, dash pattern, line cap). A zero width signals
358 /// a fill (the rasterizer branches on it).
359 pub stroke_style: StrokeStyle,
360 /// Fill rule for the fill branch (ignored when stroking).
361 pub fill_rule: FillRule,
362 /// Bounding rect in logical pixels (computed from path bounds).
363 pub bounds: [f32; 4],
364 /// Paint type for the fill. Mirrors `ShapeQuad::paint_data` (Tier 2):
365 /// `Solid` fills draw through the lean `quad_pipeline` (tinted by
366 /// `color`); gradient variants draw through the dedicated
367 /// `path_gradient` pipeline, which ignores `color` and instead
368 /// samples an analytic gradient using this data.
369 ///
370 /// Strokes may carry a gradient too (`Canvas::stroke_path_with_paint`): the
371 /// gradient pipeline samples the atlas coverage mask, and the mask is the
372 /// stroked outline rather than the filled interior — nothing else about the
373 /// draw changes. Note the gradient is normalized against `bounds`, which for
374 /// a stroke is the outline's *expanded* rect; the canvas re-bases the
375 /// gradient's coordinates accordingly, so callers always express them
376 /// relative to the path's own bounds.
377 pub paint_data: PaintData,
378}
379
380/// Paint data for SDF shapes, passed to the GPU shader.
381#[derive(Debug, Default, Clone, PartialEq)]
382pub enum PaintData {
383 #[default]
384 Solid,
385 LinearGradient {
386 start: [f32; 2],
387 end: [f32; 2],
388 stops: Vec<crate::paint::GradientStop>,
389 },
390 RadialGradient {
391 center: [f32; 2],
392 radius: f32,
393 stops: Vec<crate::paint::GradientStop>,
394 },
395 ConicGradient {
396 center: [f32; 2],
397 start_angle: f32,
398 stops: Vec<crate::paint::GradientStop>,
399 },
400}
401
402/// Compositing blend mode.
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
404pub enum BlendMode {
405 #[default]
406 Normal,
407 Multiply,
408 Screen,
409 Overlay,
410 Darken,
411 Lighten,
412 ColorDodge,
413 ColorBurn,
414}
415
416/// A draw command referencing an entry in one of the RenderFrame arrays.
417/// Commands are recorded in painter's order (back-to-front).
418#[derive(Debug, Clone, PartialEq)]
419pub enum DrawCommand {
420 Glyph(usize),
421 Image(usize),
422 Decoration(usize),
423 /// A transform-invariant cosmetic line — index into
424 /// [`RenderFrame::cosmetic_lines`].
425 CosmeticLine(usize),
426 Shape(usize),
427 Shadow(usize),
428 Rasterized(usize),
429 Path(usize),
430 /// Shader-driven animated quad — index into `RenderFrame::animated_quads`.
431 /// The per-frame state (phase, frame_index, colors) is NOT in the
432 /// vertex data; the renderer looks it up from its uniform buffer
433 /// via the `slot` stored in `AnimatedQuadDraw`.
434 AnimatedQuad(usize),
435 SetClip(Rect),
436 ClearClip,
437 SetOpacity(f32),
438 RestoreOpacity,
439 SetBlendMode(BlendMode),
440 RestoreBlendMode,
441 /// Set the renderer's current transform. **Composes** with the
442 /// top of the renderer's transform stack: the new current is
443 /// `t.then(stack_top)` — the supplied transform is applied to a
444 /// local point *first*, then the stack's outer ancestors compose
445 /// outward. For widgets not under any `PushTransform` scope the
446 /// stack is `[identity]`, so this behaves as "set absolute" —
447 /// backwards compatible.
448 SetTransform(Transform2D),
449 /// Push a new transform onto the renderer's transform stack.
450 /// The new top becomes `t.then(prev_top)` — the deepest (innermost)
451 /// `t` applies to a pre-transform local point first, then outer
452 /// ancestors compose outward. That becomes the renderer's
453 /// `current_transform` until the matching
454 /// [`DrawCommand::PopTransform`]. Emitted by the render walker
455 /// around a subtree whose root has a `transform_prop` set. See
456 /// `WidgetArena::effective_transform` in `teksilo-core` for the
457 /// composition mirrored on the arena side (used by hit-testing
458 /// and a11y bounds projection).
459 PushTransform(Transform2D),
460 /// Pop the renderer's transform stack, restoring the previous
461 /// top as the new `current_transform`. Must be paired with a
462 /// [`DrawCommand::PushTransform`].
463 PopTransform,
464 /// Begin an offscreen-rendered, blurred subtree. The renderer
465 /// allocates an intermediate texture sized to `bounds` (in logical
466 /// pixels), redirects subsequent drawing into it, and on the
467 /// matching [`DrawCommand::EndBlurredSubtree`] runs a dual-Kawase
468 /// blur chain at the requested `radius` and composites the result
469 /// back into the parent pass at `bounds`.
470 BeginBlurredSubtree {
471 bounds: Rect,
472 radius: f32,
473 },
474 /// End an offscreen-rendered, blurred subtree. Must be paired with
475 /// a preceding [`DrawCommand::BeginBlurredSubtree`].
476 EndBlurredSubtree,
477}
478
479/// An animated quad to render with one of the shader-animation pipelines.
480/// The fragment shader samples per-slot state from the renderer's uniform
481/// buffer (updated each frame by the widget tree's animated-quad
482/// registry) — the `slot` field selects which entry to read.
483#[derive(Debug, Clone, PartialEq)]
484pub struct AnimatedQuadDraw {
485 /// Screen-space bounds: [x, y, width, height] in logical pixels.
486 pub screen: [f32; 4],
487 /// Dense index into the renderer's `AnimParams` uniform array. Owned
488 /// and allocated by the widget tree's `AnimatedQuadRegistry`; stable
489 /// for the lifetime of one widget mount (freed on rebuild/destroy).
490 pub slot: u32,
491 /// Which pipeline draws this quad — procedural (sweep, pulse…) or
492 /// sprite (texture-atlas frame cycling). Picked once at emit time.
493 pub class: AnimatedQuadClass,
494}
495
496/// Which shader pipeline a [`DrawCommand::AnimatedQuad`] is routed to.
497/// Chosen by the widget at `Canvas::draw_animated_quad` time based on
498/// its `AnimatedQuadKind`; the renderer binds the matching pipeline.
499#[derive(Debug, Clone, PartialEq)]
500pub enum AnimatedQuadClass {
501 /// Fully procedural — no texture binding. IndeterminateSweep,
502 /// Pulse, Shimmer, etc.
503 Procedural,
504 /// Samples a texture atlas. Carries the image name so the renderer
505 /// can resolve the bind group (same path registered images use).
506 Sprite { image_name: String },
507}
508
509/// GPU-visible per-slot state for a shader-driven animated quad.
510/// Layout must match the WGSL `AnimParams` struct in
511/// `teksilo-render/src/shaders/anim_procedural.wgsl` (and the sprite
512/// variant). `repr(C)` with explicit `_pad` fields for `std140`
513/// compatibility.
514///
515/// Lives in `teksilo-canvas` (not `teksilo-core`) because it is the
516/// serialized-over-the-wire data type between the tree's animated-quad
517/// registry and the renderer, and `RenderFrame` is already the
518/// tree→renderer data channel.
519#[repr(C)]
520#[derive(Debug, Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
521pub struct AnimParams {
522 /// Discriminator — 0 = IndeterminateSweep, 1 = SpriteCycle,
523 /// 2 = SpinnerArc, ... Matches the `kind: u32` constant in the
524 /// fragment shader switch.
525 pub kind: u32,
526 /// Continuous phase for procedural kinds (0..1) OR integer frame
527 /// index (as f32) for sprite kinds. SpinnerArc: rotation phase
528 /// (0..1, one full rotation per period).
529 pub phase: f32,
530 /// IndeterminateSweep: sweep band width (0..1).
531 /// SpinnerArc: arc length as a fraction of the full circle.
532 /// Other kinds unused.
533 pub sweep_ratio: f32,
534 /// Generic per-kind parameter slot. SpinnerArc: stroke thickness
535 /// as a fraction of the smaller extent (0..0.5). Other kinds
536 /// treat this as padding for std140 alignment.
537 pub _pad0: f32,
538 /// IndeterminateSweep: track color. Unused for sprite and spinner.
539 pub color0: [f32; 4],
540 /// IndeterminateSweep: fill color. SpriteCycle: tint (alpha 0 = no
541 /// tint). SpinnerArc: arc color.
542 pub color1: [f32; 4],
543 /// Sprite atlas grid width (cols). Unused for procedural.
544 pub atlas_cols: f32,
545 /// Sprite atlas grid height (rows). Unused for procedural.
546 pub atlas_rows: f32,
547 pub _pad1: [f32; 2],
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 #[test]
555 fn merge_frames() {
556 let mut a = RenderFrame::new();
557 a.shapes.push(ShapeQuad {
558 screen: [0.0, 0.0, 10.0, 10.0],
559 color: [1.0, 0.0, 0.0, 1.0],
560 shape: ShapeKind::RoundedRect,
561 stroke_width: 0.0,
562 stroke_space: StrokeSpace::Logical,
563 corner_radii: [0.0; 4],
564 paint_data: PaintData::Solid,
565 });
566 a.draw_order.push(DrawCommand::Shape(0));
567
568 let mut b = RenderFrame::new();
569 b.decorations.push(DecorationRect {
570 rect: [0.0, 0.0, 5.0, 5.0],
571 color: [0.0, 0.0, 1.0, 1.0],
572 kind: DecorationKind::FocusRing,
573 });
574 b.draw_order.push(DrawCommand::Decoration(0));
575
576 a.merge(&b);
577 assert_eq!(a.shapes.len(), 1);
578 assert_eq!(a.decorations.len(), 1);
579 assert_eq!(a.draw_order.len(), 2);
580 assert_eq!(a.draw_order[1], DrawCommand::Decoration(0));
581 }
582
583 #[test]
584 fn merge_preserves_state_commands() {
585 let mut a = RenderFrame::new();
586 a.draw_order.push(DrawCommand::SetOpacity(0.5));
587 let mut b = RenderFrame::new();
588 b.draw_order.push(DrawCommand::RestoreOpacity);
589 a.merge(&b);
590 assert_eq!(a.draw_order.len(), 2);
591 assert_eq!(a.draw_order[0], DrawCommand::SetOpacity(0.5));
592 assert_eq!(a.draw_order[1], DrawCommand::RestoreOpacity);
593 }
594}