Skip to main content

fission_render/
lib.rs

1use fission_ir::op::{EmbedKind, RichTextAnnotation, TextParagraphStyle};
2use fission_ir::{NodeId, WidgetNodeId};
3pub use fission_layout::{LayoutPoint, LayoutRect, LayoutSize, LayoutUnit};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
7pub struct Color {
8    pub r: u8,
9    pub g: u8,
10    pub b: u8,
11    pub a: u8,
12}
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub enum Fill {
16    Solid(Color),
17    LinearGradient {
18        start: (f32, f32),
19        end: (f32, f32),
20        stops: Vec<(f32, Color)>,
21    },
22    RadialGradient {
23        center: (f32, f32),
24        radius: f32,
25        stops: Vec<(f32, Color)>,
26    },
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
30pub enum LineCap {
31    Butt,
32    Round,
33    Square,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub enum LineJoin {
38    Miter,
39    Round,
40    Bevel,
41}
42
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44pub struct Stroke {
45    pub fill: Fill,
46    pub width: LayoutUnit,
47    pub dash_array: Option<Vec<f32>>,
48    pub line_cap: LineCap,
49    pub line_join: LineJoin,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
53pub struct BoxShadow {
54    pub color: Color,
55    pub blur_radius: LayoutUnit,
56    pub offset: (LayoutUnit, LayoutUnit),
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
60pub enum ImageFit {
61    Contain,
62    Cover,
63    Fill,
64    None,
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub struct TextStyle {
69    pub font_size: LayoutUnit,
70    pub color: Color,
71    pub underline: bool,
72    pub font_family: Option<String>,
73    pub locale: Option<String>,
74    pub font_weight: u16,
75    pub font_style: fission_ir::op::FontStyle,
76    pub line_height: Option<LayoutUnit>,
77    pub letter_spacing: LayoutUnit,
78    /// Optional background highlight color for this run.
79    pub background_color: Option<Color>,
80}
81
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83pub struct TextRun {
84    pub text: String,
85    pub style: TextStyle,
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89pub enum DisplayOp {
90    Save,
91    Restore,
92    ClipRect(LayoutRect),
93    ClipRoundedRect {
94        rect: LayoutRect,
95        radius: LayoutUnit,
96    },
97    OpacityLayer {
98        alpha: f32,
99        bounds: LayoutRect,
100    },
101    Translate(LayoutPoint),
102    Transform([LayoutUnit; 16]),
103    CachedScene {
104        cache_key: u64,
105        bounds: LayoutRect,
106        list: Box<DisplayList>,
107    },
108    DrawRect {
109        rect: LayoutRect,
110        fill: Option<Fill>,
111        stroke: Option<Stroke>,
112        corner_radius: LayoutUnit,
113        shadow: Option<BoxShadow>,
114        bounds: LayoutRect,
115        node_id: Option<NodeId>,
116    },
117    DrawText {
118        text: String,
119        position: LayoutPoint,
120        size: LayoutUnit,
121        color: Color,
122        bounds: LayoutRect,
123        node_id: Option<NodeId>,
124        underline: bool,
125        wrap: bool,
126        caret_index: Option<usize>,
127        caret_color: Option<Color>,
128        caret_width: Option<LayoutUnit>,
129        caret_height: Option<LayoutUnit>,
130        caret_radius: Option<LayoutUnit>,
131        paragraph_style: Option<TextParagraphStyle>,
132    },
133    DrawRichText {
134        runs: Vec<TextRun>,
135        position: LayoutPoint,
136        bounds: LayoutRect,
137        node_id: Option<NodeId>,
138        wrap: bool,
139        caret_index: Option<usize>,
140        caret_color: Option<Color>,
141        caret_width: Option<LayoutUnit>,
142        caret_height: Option<LayoutUnit>,
143        caret_radius: Option<LayoutUnit>,
144        paragraph_style: Option<TextParagraphStyle>,
145        #[serde(default)]
146        annotations: Vec<RichTextAnnotation>,
147    },
148    DrawImage {
149        rect: LayoutRect,
150        source: String,
151        fit: ImageFit,
152        bounds: LayoutRect,
153        node_id: Option<NodeId>,
154    },
155    DrawPath {
156        path: String,
157        fill: Option<Fill>,
158        stroke: Option<Stroke>,
159        bounds: LayoutRect,
160        node_id: Option<NodeId>,
161    },
162    DrawSvg {
163        content: String,
164        fill: Option<Fill>,
165        stroke: Option<Stroke>,
166        bounds: LayoutRect,
167        node_id: Option<NodeId>,
168    },
169    DrawSurface {
170        rect: LayoutRect,
171        surface_id: u64,
172        position: u64,
173        bounds: LayoutRect,
174        node_id: Option<NodeId>,
175    },
176}
177
178pub fn embed_surface_id(kind: &EmbedKind, widget_id: WidgetNodeId) -> u64 {
179    let kind_tag = match kind {
180        EmbedKind::Video => 0xF151_0000_0000_0001,
181        EmbedKind::Web => 0xF151_0000_0000_0002,
182        EmbedKind::Custom(_) => 0xF151_0000_0000_0003,
183    };
184    let raw = widget_id.as_u128();
185    (raw as u64) ^ ((raw >> 64) as u64).rotate_left(13) ^ kind_tag
186}
187
188pub fn surface_placeholder_color(surface_id: u64, position: u64) -> Color {
189    Color {
190        r: (surface_id.wrapping_mul(50).wrapping_add(position / 20) % 255) as u8,
191        g: (surface_id.wrapping_mul(30).wrapping_add(position / 30) % 255) as u8,
192        b: (surface_id.wrapping_mul(70).wrapping_add(position / 40) % 255) as u8,
193        a: 255,
194    }
195}
196
197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
198pub struct DisplayList {
199    pub ops: Vec<DisplayOp>,
200    pub bounds: LayoutRect,
201}
202
203impl DisplayList {
204    pub fn new(bounds: LayoutRect) -> Self {
205        Self {
206            ops: Vec::new(),
207            bounds,
208        }
209    }
210
211    pub fn push(&mut self, op: DisplayOp) {
212        self.ops.push(op);
213    }
214}
215
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
217pub enum LayerClip {
218    Rect(LayoutRect),
219    RoundedRect {
220        rect: LayoutRect,
221        radius: LayoutUnit,
222    },
223}
224
225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226pub struct LayerStyle {
227    pub clip: Option<LayerClip>,
228    pub opacity: f32,
229    pub transform: Option<[LayoutUnit; 16]>,
230    pub transform_clip: bool,
231    pub cache_key: Option<u64>,
232    pub content_cache_key: Option<u64>,
233}
234
235impl Default for LayerStyle {
236    fn default() -> Self {
237        Self {
238            clip: None,
239            opacity: 1.0,
240            transform: None,
241            transform_clip: true,
242            cache_key: None,
243            content_cache_key: None,
244        }
245    }
246}
247
248#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
249pub enum RenderNode {
250    Layer(RenderLayer),
251    Paint(DisplayList),
252}
253
254#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
255pub struct RenderLayer {
256    pub node_id: Option<NodeId>,
257    pub bounds: LayoutRect,
258    pub style: LayerStyle,
259    pub children: Vec<RenderNode>,
260}
261
262impl RenderLayer {
263    pub fn new(bounds: LayoutRect) -> Self {
264        Self {
265            node_id: None,
266            bounds,
267            style: LayerStyle::default(),
268            children: Vec::new(),
269        }
270    }
271}
272
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274pub struct RenderScene {
275    pub bounds: LayoutRect,
276    pub roots: Vec<RenderNode>,
277}
278
279impl RenderScene {
280    pub fn new(bounds: LayoutRect) -> Self {
281        Self {
282            bounds,
283            roots: Vec::new(),
284        }
285    }
286
287    pub fn from_display_list(display_list: DisplayList) -> Self {
288        Self {
289            bounds: display_list.bounds,
290            roots: vec![RenderNode::Paint(display_list)],
291        }
292    }
293
294    pub fn flatten(&self) -> DisplayList {
295        let mut list = DisplayList::new(self.bounds);
296        for root in &self.roots {
297            flatten_render_node(root, &mut list.ops);
298        }
299        list
300    }
301}
302
303fn flatten_render_node(node: &RenderNode, out: &mut Vec<DisplayOp>) {
304    match node {
305        RenderNode::Paint(list) => out.extend(list.ops.clone()),
306        RenderNode::Layer(layer) => {
307            let needs_save = layer.style.clip.is_some()
308                || layer.style.transform.is_some()
309                || (layer.style.opacity - 1.0).abs() > 0.001;
310            if needs_save {
311                out.push(DisplayOp::Save);
312            }
313            if let Some(clip) = &layer.style.clip {
314                match clip {
315                    LayerClip::Rect(rect) => out.push(DisplayOp::ClipRect(*rect)),
316                    LayerClip::RoundedRect { rect, radius } => {
317                        out.push(DisplayOp::ClipRoundedRect {
318                            rect: *rect,
319                            radius: *radius,
320                        })
321                    }
322                }
323            }
324            if (layer.style.opacity - 1.0).abs() > 0.001 {
325                out.push(DisplayOp::OpacityLayer {
326                    alpha: layer.style.opacity,
327                    bounds: layer.bounds,
328                });
329            }
330            if let Some(transform) = layer.style.transform {
331                out.push(DisplayOp::Transform(transform));
332            }
333            for child in &layer.children {
334                flatten_render_node(child, out);
335            }
336            if needs_save {
337                out.push(DisplayOp::Restore);
338            }
339        }
340    }
341}
342
343pub trait Renderer {
344    fn render_scene(&mut self, scene: &RenderScene) -> anyhow::Result<()>;
345
346    fn render(&mut self, display_list: &DisplayList) -> anyhow::Result<()> {
347        self.render_scene(&RenderScene::from_display_list(display_list.clone()))
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::{embed_surface_id, surface_placeholder_color};
354    use fission_ir::{EmbedKind, WidgetNodeId};
355
356    #[test]
357    fn embed_surface_id_is_stable_and_kind_specific() {
358        let id = WidgetNodeId::explicit("embed.demo");
359
360        assert_eq!(
361            embed_surface_id(&EmbedKind::Video, id),
362            embed_surface_id(&EmbedKind::Video, id)
363        );
364        assert_ne!(
365            embed_surface_id(&EmbedKind::Video, id),
366            embed_surface_id(&EmbedKind::Web, id)
367        );
368    }
369
370    #[test]
371    fn surface_placeholder_color_uses_wrapping_arithmetic() {
372        let color = surface_placeholder_color(u64::MAX, u64::MAX);
373
374        assert_eq!(color.a, 255);
375    }
376}