1use std::fmt;
7use std::sync::Arc;
8
9use crate::{Color, Point, Rect};
10
11#[derive(Clone, Copy, Debug, PartialEq)]
13#[non_exhaustive]
14#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
15pub enum Brush {
16 Solid(Color),
18 LinearGradient(LinearGradient),
20}
21
22impl From<Color> for Brush {
23 fn from(c: Color) -> Self {
24 Self::Solid(c)
25 }
26}
27
28impl From<LinearGradient> for Brush {
29 fn from(g: LinearGradient) -> Self {
30 Self::LinearGradient(g)
31 }
32}
33
34#[derive(Clone, Copy, Debug, PartialEq)]
37#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
38pub struct LinearGradient {
39 pub start: Point,
41 pub end: Point,
43 pub from: Color,
45 pub to: Color,
47}
48
49#[cfg_attr(feature = "live", ravenrs_live::expose)]
50impl LinearGradient {
51 pub const fn vertical(from: Color, to: Color) -> Self {
53 Self { start: Point::new(0.0, 0.0), end: Point::new(0.0, 1.0), from, to }
54 }
55}
56
57#[derive(Clone, Copy, Debug, PartialEq)]
59#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
60pub struct Border {
61 pub width: f32,
63 pub color: Color,
65}
66
67#[derive(Clone, Copy, Debug, PartialEq)]
70#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
71pub struct Shadow {
72 pub offset: Point,
74 pub blur: f32,
76 pub color: Color,
78}
79
80#[cfg_attr(feature = "live", ravenrs_live::expose)]
81impl Shadow {
82 pub const fn soft() -> Self {
84 Self { offset: Point::new(0.0, 8.0), blur: 24.0, color: Color::rgba(0.0, 0.0, 0.0, 0.35) }
85 }
86
87 pub const fn raised() -> Self {
89 Self { offset: Point::new(0.0, 16.0), blur: 40.0, color: Color::rgba(0.0, 0.0, 0.0, 0.45) }
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq)]
96#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
97pub struct Quad {
98 pub rect: Rect,
100 pub corner_radii: [f32; 4],
102 pub fill: Brush,
104 pub border: Option<Border>,
106 pub shadow: Option<Shadow>,
108}
109
110#[cfg_attr(feature = "live", ravenrs_live::expose)]
111impl Quad {
112 pub fn new(rect: Rect, fill: impl Into<Brush>) -> Self {
122 Self { rect, corner_radii: [0.0; 4], fill: fill.into(), border: None, shadow: None }
123 }
124
125 pub fn radius(mut self, r: f32) -> Self {
127 self.corner_radii = [r; 4];
128 self
129 }
130
131 pub fn border(mut self, width: f32, color: Color) -> Self {
133 self.border = Some(Border { width, color });
134 self
135 }
136
137 pub fn shadow(mut self, shadow: Shadow) -> Self {
139 self.shadow = Some(shadow);
140 self
141 }
142}
143
144#[derive(Clone)]
146#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
147pub struct FontRef {
148 pub id: u64,
150 pub data: Arc<dyn AsRef<[u8]> + Send + Sync>,
152 pub index: u32,
154}
155
156#[cfg_attr(feature = "live", ravenrs_live::expose)]
157impl FontRef {
158 pub fn bytes(&self) -> &[u8] {
160 (*self.data).as_ref()
161 }
162}
163
164impl fmt::Debug for FontRef {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 f.debug_struct("FontRef").field("id", &self.id).field("index", &self.index).finish()
167 }
168}
169
170impl PartialEq for FontRef {
171 fn eq(&self, other: &Self) -> bool {
172 self.id == other.id && self.index == other.index
173 }
174}
175
176#[derive(Clone, Copy, Debug, PartialEq)]
178#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
179pub struct PositionedGlyph {
180 pub id: u32,
182 pub x: f32,
184 pub y: f32,
186}
187
188#[derive(Clone, Debug, PartialEq)]
190#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
191pub struct GlyphRun {
192 pub font: FontRef,
194 pub size: f32,
196 pub color: Color,
198 pub glyphs: Vec<PositionedGlyph>,
200}
201
202#[derive(Clone)]
205#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
206pub struct ImageData {
207 id: u64,
208 width: u32,
209 height: u32,
210 pixels: Arc<[u8]>,
211}
212
213#[cfg_attr(feature = "live", ravenrs_live::expose)]
214impl ImageData {
215 pub fn from_rgba8(width: u32, height: u32, pixels: impl Into<Arc<[u8]>>) -> Option<Self> {
217 let pixels = pixels.into();
218 if width == 0 || height == 0 || pixels.len() != width as usize * height as usize * 4 {
219 return None;
220 }
221 static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
222 let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
223 Some(Self { id, width, height, pixels })
224 }
225
226 pub fn from_fn(width: u32, height: u32, f: impl Fn(u32, u32) -> Color) -> Option<Self> {
228 let mut pixels = Vec::with_capacity(width as usize * height as usize * 4);
229 for y in 0..height {
230 for x in 0..width {
231 let c = f(x, y);
232 let to8 = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
233 pixels.extend_from_slice(&[to8(c.r), to8(c.g), to8(c.b), to8(c.a)]);
234 }
235 }
236 Self::from_rgba8(width, height, pixels)
237 }
238
239 pub fn id(&self) -> u64 {
241 self.id
242 }
243
244 pub fn width(&self) -> u32 {
246 self.width
247 }
248
249 pub fn height(&self) -> u32 {
251 self.height
252 }
253
254 pub fn pixels(&self) -> &[u8] {
256 &self.pixels
257 }
258
259 pub fn aspect_ratio(&self) -> f32 {
261 self.width as f32 / self.height as f32
262 }
263}
264
265impl fmt::Debug for ImageData {
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 f.debug_struct("ImageData").field("id", &self.id).field("size", &(self.width, self.height)).finish()
268 }
269}
270
271impl PartialEq for ImageData {
272 fn eq(&self, other: &Self) -> bool {
273 self.id == other.id
274 }
275}
276
277#[derive(Clone, Debug, PartialEq)]
279#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
280pub struct ImageQuad {
281 pub rect: Rect,
283 pub image: ImageData,
285 pub uv: [f32; 4],
287 pub corner_radii: [f32; 4],
289 pub opacity: f32,
291}
292
293#[cfg_attr(feature = "live", ravenrs_live::expose)]
294impl ImageQuad {
295 pub fn new(rect: Rect, image: ImageData) -> Self {
297 Self { rect, image, uv: [0.0, 0.0, 1.0, 1.0], corner_radii: [0.0; 4], opacity: 1.0 }
298 }
299
300 pub fn radius(mut self, r: f32) -> Self {
302 self.corner_radii = [r; 4];
303 self
304 }
305}
306
307#[derive(Clone, Copy, Debug, PartialEq)]
309#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
310pub enum PathElement {
311 MoveTo(Point),
313 LineTo(Point),
315 CubicTo(Point, Point, Point),
317 Close,
319}
320
321#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
323#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
324pub enum FillRule {
325 #[default]
327 NonZero,
328 EvenOdd,
330}
331
332#[derive(Clone, Copy, Debug, PartialEq)]
334#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
335pub struct ColorStop {
336 pub offset: f32,
338 pub color: Color,
340}
341
342#[derive(Clone, Debug, PartialEq)]
344#[non_exhaustive]
345#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
346pub enum PathPaint {
347 Solid(Color),
349 Linear {
351 start: Point,
353 end: Point,
355 stops: Arc<[ColorStop]>,
357 },
358 Radial {
360 center: Point,
362 radius: f32,
364 stops: Arc<[ColorStop]>,
366 },
367 Image {
369 image: ImageData,
371 placement: [f32; 6],
374 },
375}
376
377#[derive(Clone, Debug, PartialEq)]
379#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
380pub struct PathClip {
381 pub elements: Arc<[PathElement]>,
383 pub rule: FillRule,
385 pub inverted: bool,
387}
388
389#[derive(Clone, Debug, PartialEq)]
392#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
393pub struct PathFill {
394 pub elements: Arc<[PathElement]>,
396 pub rule: FillRule,
398 pub paint: PathPaint,
400 pub transform: [f32; 6],
403 pub opacity: f32,
405 pub clip: Option<Arc<PathClip>>,
407}
408
409#[cfg_attr(feature = "live", ravenrs_live::expose)]
410impl PathFill {
411 pub const IDENTITY: [f32; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
413
414 pub fn solid(elements: impl Into<Arc<[PathElement]>>, color: Color) -> Self {
416 Self {
417 elements: elements.into(),
418 rule: FillRule::NonZero,
419 paint: PathPaint::Solid(color),
420 transform: Self::IDENTITY,
421 opacity: 1.0,
422 clip: None,
423 }
424 }
425}
426
427#[derive(Clone, Debug, PartialEq)]
429#[non_exhaustive]
430#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
431pub enum Primitive {
432 Quad(Quad),
434 Glyphs(GlyphRun),
436 Image(ImageQuad),
438 Path(PathFill),
440 PushClip(Rect),
443 PopClip,
445}
446
447#[derive(Clone, Copy, Debug, PartialEq)]
450#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
451pub struct Effect {
452 pub opacity: f32,
455 pub offset: Point,
457 pub scale: f32,
459}
460
461#[cfg_attr(feature = "live", ravenrs_live::expose)]
462impl Effect {
463 pub const IDENTITY: Self = Self { opacity: 1.0, offset: Point::new(0.0, 0.0), scale: 1.0 };
465
466 pub const fn hidden() -> Self {
468 Self { opacity: 0.0, ..Self::IDENTITY }
469 }
470
471 pub const fn moved(x: f32, y: f32) -> Self {
473 Self { offset: Point::new(x, y), ..Self::IDENTITY }
474 }
475
476 pub const fn scaled(scale: f32) -> Self {
478 Self { scale, ..Self::IDENTITY }
479 }
480
481 pub const fn with_opacity(mut self, opacity: f32) -> Self {
483 self.opacity = opacity;
484 self
485 }
486
487 pub const fn with_scale(mut self, scale: f32) -> Self {
489 self.scale = scale;
490 self
491 }
492
493 pub fn lerp(self, to: Self, t: f32) -> Self {
496 let mix = |a: f32, b: f32| a + (b - a) * t;
497 Self {
498 opacity: mix(self.opacity, to.opacity).clamp(0.0, 1.0),
499 offset: Point::new(mix(self.offset.x, to.offset.x), mix(self.offset.y, to.offset.y)),
500 scale: mix(self.scale, to.scale).max(0.0),
501 }
502 }
503
504 pub fn then(self, next: Self) -> Self {
507 Self {
508 opacity: self.opacity * next.opacity,
509 offset: Point::new(self.offset.x + next.offset.x, self.offset.y + next.offset.y),
510 scale: self.scale * next.scale,
511 }
512 }
513
514 pub fn is_identity(&self) -> bool {
516 *self == Self::IDENTITY
517 }
518
519 fn point(&self, p: Point, pivot: Point) -> Point {
520 Point::new(
521 pivot.x + (p.x - pivot.x) * self.scale + self.offset.x,
522 pivot.y + (p.y - pivot.y) * self.scale + self.offset.y,
523 )
524 }
525
526 fn rect(&self, r: Rect, pivot: Point) -> Rect {
527 Rect::from_origin_size(self.point(r.origin, pivot), r.size.scale(self.scale))
528 }
529
530 fn color(&self, c: Color) -> Color {
531 c.with_alpha(c.a * self.opacity)
532 }
533
534 fn apply(&self, primitive: &mut Primitive, pivot: Point) {
535 match primitive {
536 Primitive::Quad(q) => {
537 q.rect = self.rect(q.rect, pivot);
538 q.corner_radii = q.corner_radii.map(|r| r * self.scale);
539 q.fill = match q.fill {
540 Brush::Solid(c) => Brush::Solid(self.color(c)),
541 Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradient {
542 from: self.color(g.from),
543 to: self.color(g.to),
544 ..g
545 }),
546 };
547 if let Some(b) = &mut q.border {
548 b.width *= self.scale;
549 b.color = self.color(b.color);
550 }
551 if let Some(s) = &mut q.shadow {
552 s.offset = Point::new(s.offset.x * self.scale, s.offset.y * self.scale);
553 s.blur *= self.scale;
554 s.color = self.color(s.color);
555 }
556 }
557 Primitive::Glyphs(run) => {
558 run.size *= self.scale;
559 run.color = self.color(run.color);
560 for g in &mut run.glyphs {
561 let p = self.point(Point::new(g.x, g.y), pivot);
562 (g.x, g.y) = (p.x, p.y);
563 }
564 }
565 Primitive::Path(path) => {
566 path.opacity *= self.opacity;
567 let [a, b, c, d, e, f] = path.transform;
568 let k = self.scale;
569 path.transform = [
570 a * k,
571 b * k,
572 c * k,
573 d * k,
574 pivot.x + self.offset.x + (e - pivot.x) * k,
575 pivot.y + self.offset.y + (f - pivot.y) * k,
576 ];
577 }
578 Primitive::Image(image) => {
579 image.rect = self.rect(image.rect, pivot);
580 image.corner_radii = image.corner_radii.map(|r| r * self.scale);
581 image.opacity *= self.opacity;
582 }
583 Primitive::PushClip(rect) => *rect = self.rect(*rect, pivot),
584 Primitive::PopClip => {}
585 }
586 }
587}
588
589impl Default for Effect {
590 fn default() -> Self {
591 Self::IDENTITY
592 }
593}
594
595#[derive(Clone, Debug, Default)]
597#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
598pub struct Scene {
599 primitives: Vec<Primitive>,
600 clear_color: Color,
601}
602
603#[cfg(feature = "live")]
606impl ravenrs_live::bridge::Scratch for Scene {
607 fn fresh() -> Self {
608 Self::new()
609 }
610
611 fn merge_into(self, target: &mut Self) {
612 target.extend_from_slice(self.primitives());
613 }
614}
615
616#[cfg_attr(feature = "live", ravenrs_live::expose)]
617impl Scene {
618 pub fn new() -> Self {
620 Self::default()
621 }
622
623 pub fn clear(&mut self) {
625 self.primitives.clear();
626 }
627
628 pub fn set_clear_color(&mut self, color: Color) {
630 self.clear_color = color;
631 }
632
633 pub fn clear_color(&self) -> Color {
635 self.clear_color
636 }
637
638 pub fn push_path(&mut self, path: PathFill) {
640 self.primitives.push(Primitive::Path(path));
641 }
642
643 pub fn push_quad(&mut self, quad: Quad) {
645 self.primitives.push(Primitive::Quad(quad));
646 }
647
648 pub fn push_glyphs(&mut self, run: GlyphRun) {
650 if !run.glyphs.is_empty() {
651 self.primitives.push(Primitive::Glyphs(run));
652 }
653 }
654
655 pub fn push_image(&mut self, image: ImageQuad) {
657 self.primitives.push(Primitive::Image(image));
658 }
659
660 pub fn push_clip(&mut self, rect: Rect) {
663 self.primitives.push(Primitive::PushClip(rect));
664 }
665
666 pub fn pop_clip(&mut self) {
668 self.primitives.push(Primitive::PopClip);
669 }
670
671 pub fn apply_effect(&mut self, range: std::ops::Range<usize>, effect: Effect, pivot: Point) {
683 if effect.is_identity() {
684 return;
685 }
686 if let Some(primitives) = self.primitives.get_mut(range) {
687 for primitive in primitives {
688 effect.apply(primitive, pivot);
689 }
690 }
691 }
692
693 pub fn extend_from_slice(&mut self, primitives: &[Primitive]) {
695 self.primitives.extend_from_slice(primitives);
696 }
697
698 pub fn primitives(&self) -> &[Primitive] {
700 &self.primitives
701 }
702
703 pub fn len(&self) -> usize {
705 self.primitives.len()
706 }
707
708 pub fn is_empty(&self) -> bool {
710 self.primitives.is_empty()
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717
718 #[test]
719 fn image_data_validates_size_and_gets_unique_ids() {
720 assert!(ImageData::from_rgba8(2, 2, vec![0u8; 15]).is_none());
721 let a = ImageData::from_rgba8(2, 2, vec![0u8; 16]).unwrap();
722 let b = ImageData::from_fn(2, 1, |x, _| if x == 0 { Color::WHITE } else { Color::BLACK }).unwrap();
723 assert_ne!(a.id(), b.id());
724 assert_eq!(&b.pixels()[..8], &[255, 255, 255, 255, 0, 0, 0, 255]);
725 assert_eq!(a, a.clone());
726 }
727
728 #[test]
729 fn empty_glyph_runs_are_skipped() {
730 let mut scene = Scene::new();
731 scene.push_glyphs(GlyphRun {
732 font: FontRef { id: 1, data: Arc::new(Vec::<u8>::new()), index: 0 },
733 size: 12.0,
734 color: Color::WHITE,
735 glyphs: vec![],
736 });
737 assert!(scene.is_empty());
738 scene.push_quad(Quad::new(Rect::new(0.0, 0.0, 1.0, 1.0), Color::BLACK).radius(4.0));
739 assert_eq!(scene.len(), 1);
740 scene.clear();
741 assert!(scene.is_empty());
742 }
743}