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 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}