Skip to main content

fission_render/
lib.rs

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