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