1use anyhow::Result;
2use fission_core::internal::build_layout_tree;
3use fission_core::internal::BuildCtx;
4use fission_core::internal::InternalLoweringCx;
5use fission_core::{
6 Action, ActionEnvelope, ActionId, AdvanceTo, Clock, CurrentTime, Env, GlobalState, InputEvent,
7 LayoutPoint, Runtime, ScrollStateMap, View, Widget, WidgetIdExt,
8};
9use fission_ir::{CoreIR, WidgetId};
10use fission_layout::{LayoutEngine, LayoutInputNode, LayoutSize, LayoutSnapshot, TextMeasurer};
11use fission_render::{
12 embed_surface_id, BoxShadow, Color, DisplayList, DisplayOp, LayoutRect, RenderScene, Renderer,
13};
14use fission_render_vello::parley::FontContext;
15use fission_render_vello::VelloTextMeasurer;
16use fission_theme::fonts;
17use fontique::{Blob, Collection, CollectionOptions, FontInfoOverride, SourceCache};
18use std::sync::{Arc, Mutex};
19
20pub fn layout_input_nodes(ir: &CoreIR, env: &Env) -> Vec<LayoutInputNode> {
21 build_layout_tree(ir, env)
22}
23
24#[derive(Default, Clone)]
26pub struct MockRenderer {
27 pub last_display_list: Arc<Mutex<Option<DisplayList>>>,
28}
29
30impl Renderer for MockRenderer {
31 fn render_scene(&mut self, scene: &RenderScene) -> Result<()> {
32 let mut lock = self.last_display_list.lock().unwrap();
33 *lock = Some(scene.flatten());
34 Ok(())
35 }
36}
37
38struct MockTextMeasurer;
39impl TextMeasurer for MockTextMeasurer {
40 fn measure(&self, text: &str, _font_size: f32, avail: Option<f32>) -> (f32, f32) {
41 let char_width = 10.0;
42 let line_height = 20.0;
43 let full_width = text.len() as f32 * char_width;
44
45 if let Some(w) = avail {
46 if full_width > w {
47 let safe_w = w.max(char_width);
50 let lines = (full_width / safe_w).ceil();
51 return (w, lines * line_height);
52 }
53 }
54 (full_width, line_height)
55 }
56 fn hit_test(
57 &self,
58 _text: &str,
59 _font_size: f32,
60 _available_width: Option<f32>,
61 _x: f32,
62 _y: f32,
63 ) -> usize {
64 0
65 }
66 fn measure_rich_text(
67 &self,
68 runs: &[fission_ir::op::TextRun],
69 available_width: Option<f32>,
70 ) -> (f32, f32) {
71 let full_w: f32 = runs.iter().map(|r| r.text.len() as f32 * 10.0).sum();
72 let char_width = 10.0;
73 let line_height = 20.0;
74
75 if let Some(w) = available_width {
76 if full_w > w {
77 let safe_w = w.max(char_width);
78 let lines = (full_w / safe_w).ceil();
79 return (w, lines * line_height);
80 }
81 }
82 (full_w.max(10.0), line_height)
83 }
84}
85
86const DEFAULT_TEST_FONT_FAMILY: &str = "Fission Default";
87
88fn should_use_mock_measurer() -> bool {
89 let env_mock = std::env::var("FISSION_TEST_USE_MOCK_MEASURER")
90 .map(|v| {
91 let v = v.to_ascii_lowercase();
92 v == "1" || v == "true" || v == "yes"
93 })
94 .unwrap_or(false);
95 let env_kind = std::env::var("FISSION_TEST_MEASURER")
96 .map(|v| v.to_ascii_lowercase())
97 .ok();
98 env_mock || matches!(env_kind.as_deref(), Some("mock"))
99}
100
101fn build_vello_measurer() -> Arc<dyn TextMeasurer> {
102 let font_cx = Arc::new(Mutex::new(build_font_context()));
103 {
104 let mut font_cx = font_cx.lock().unwrap();
105 let font_data = fonts::default_font_bytes().to_vec();
106 let info_override = FontInfoOverride {
107 family_name: Some(DEFAULT_TEST_FONT_FAMILY),
108 ..Default::default()
109 };
110 font_cx
111 .collection
112 .register_fonts(Blob::from(font_data), Some(info_override));
113 }
114 Arc::new(VelloTextMeasurer::new_with_default_family(
115 font_cx,
116 DEFAULT_TEST_FONT_FAMILY,
117 ))
118}
119
120fn build_font_context() -> FontContext {
121 let use_system_fonts = std::env::var("FISSION_USE_SYSTEM_FONTS")
122 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
123 .unwrap_or(false);
124 let options = CollectionOptions {
125 shared: false,
126 system_fonts: use_system_fonts,
127 };
128 FontContext {
129 collection: Collection::new(options),
130 source_cache: SourceCache::default(),
131 }
132}
133
134pub mod linter;
135pub use linter::*;
136
137pub mod driver;
138pub use driver::{SemanticMatch, TestDriver, TextMatch};
139
140pub mod prelude {
141 pub use crate::linter::{LayoutLinter, LayoutViolation};
142 pub use crate::{
143 detect_ir_cycle, MockRenderer, SemanticMatch, TestDriver, TestHarness, TextMatch,
144 };
145 pub use fission_ir::semantics::{ActionTrigger, Role};
146 pub use fission_ir::{EmbedKind, LayoutOp, Op, PaintOp};
147 pub use fission_render::{DisplayList, DisplayOp};
148}
149
150pub struct TestHarness<S: GlobalState> {
151 pub runtime: Runtime,
152 pub renderer: MockRenderer,
153 pub layout_engine: LayoutEngine,
154 pub last_snapshot: Option<LayoutSnapshot>,
155 pub last_ir: Option<CoreIR>,
156 pub root_widget: Option<Box<dyn Fn() -> Widget>>,
157 pub env: Env,
158 pub measurer: Arc<dyn TextMeasurer>,
159 _phantom: std::marker::PhantomData<S>,
160}
161
162impl<S: GlobalState> TestHarness<S> {
163 pub fn lint(&self) -> Vec<LayoutViolation> {
165 if let (Some(ir), Some(snapshot)) = (&self.last_ir, &self.last_snapshot) {
166 LayoutLinter::new(ir, snapshot).check()
167 } else {
168 vec![]
169 }
170 }
171
172 pub fn new(initial_state: S) -> Self {
173 if should_use_mock_measurer() {
174 return Self::new_with_measurer(initial_state, Arc::new(MockTextMeasurer));
175 }
176 Self::new_with_measurer(initial_state, build_vello_measurer())
177 }
178
179 pub fn new_with_mock_measurer(initial_state: S) -> Self {
180 Self::new_with_measurer(initial_state, Arc::new(MockTextMeasurer))
181 }
182
183 pub fn new_with_measurer(initial_state: S, measurer: Arc<dyn TextMeasurer>) -> Self {
184 let mut runtime = Runtime::default();
185 if std::any::TypeId::of::<S>() != std::any::TypeId::of::<Clock>() {
186 runtime
187 .add_app_state(Box::new(initial_state))
188 .expect("Failed to add initial state");
189 }
190
191 Self {
192 runtime: runtime.with_measurer(measurer.clone()),
193 renderer: MockRenderer::default(),
194 layout_engine: LayoutEngine::new().with_measurer(measurer.clone()),
195 last_snapshot: None,
196 last_ir: None,
197 root_widget: None,
198 env: Env::default(),
199 measurer,
200 _phantom: std::marker::PhantomData,
201 }
202 }
203
204 pub fn with_root_widget<W>(mut self, widget: W) -> Self
205 where
206 W: Clone + Into<Widget> + 'static,
207 {
208 self.root_widget = Some(Box::new(move || widget.clone().into()));
209 self
210 }
211
212 pub fn register_reducer(
213 mut self,
214 action_id: ActionId,
215 reducer: fission_core::action::Reducer<S>,
216 ) -> Self {
217 self.runtime
218 .register_reducer::<S>(action_id, reducer)
219 .unwrap();
220 self
221 }
222
223 pub fn dispatch(&mut self, action: impl Action + 'static) -> Result<()> {
224 let target = fission_core::WidgetId::from_u128(0);
225 let envelope: ActionEnvelope = action.into();
226 self.runtime.dispatch(envelope, target)
227 }
228
229 pub fn send_event(&mut self, event: InputEvent) -> Result<()> {
230 if let (Some(ir), Some(layout)) = (&self.last_ir, &self.last_snapshot) {
231 self.runtime.handle_input(event, ir, layout)
232 } else {
233 anyhow::bail!(
234 "Cannot handle input: no frame pumped (missing IR/Layout). Call pump() first."
235 );
236 }
237 }
238
239 pub fn tick(&mut self, dt: CurrentTime) -> Result<()> {
240 self.runtime.tick(dt).map(|_| ())
241 }
242
243 pub fn advance_to(&mut self, time: CurrentTime) -> Result<()> {
244 self.dispatch(AdvanceTo { time })
245 }
246
247 pub fn current_time(&self) -> CurrentTime {
248 self.runtime.clock().current_time()
249 }
250
251 pub fn pump(&mut self) -> Result<()> {
252 let trace = std::env::var("FISSION_TEST_TRACE").ok().as_deref() == Some("1");
253 let mut viewport = LayoutSize {
254 width: 800.0,
255 height: 600.0,
256 };
257 if self.env.viewport_size.width > 0.0
258 && self.env.viewport_size.height > 0.0
259 && self.env.viewport_size.width.is_finite()
260 && self.env.viewport_size.height.is_finite()
261 {
262 viewport = self.env.viewport_size;
263 } else {
264 self.env.viewport_size = viewport;
265 }
266 if let Some(root) = self.root_widget.as_ref() {
268 if trace {
270 eprintln!("[test-trace] build start");
271 }
272 let node_tree = {
273 let state = self
274 .runtime
275 .get_app_state::<S>()
276 .expect("App state missing");
277 let view = View::new(
278 state,
279 &self.runtime.runtime_state,
280 &self.env,
281 self.last_snapshot.as_ref(),
282 );
283 let mut ctx = BuildCtx::new();
284 let tree = fission_core::build::enter(&mut ctx, &view, root);
285
286 self.runtime.clear_reducers();
287 let motion_declarations = ctx.take_motion_declarations();
288 let video_nodes = ctx.take_video_registrations();
289 let web_nodes = ctx.take_web_registrations();
290 let portals_with_ids = ctx.take_portals();
291
292 let portals = portals_with_ids
293 .into_iter()
294 .map(|(id, node)| {
295 if let Some(id) = id {
296 let wrapper_id = WidgetId::derived(id.as_u128(), &[0x0000_F001]);
298 fission_core::ui::Container::new(node)
299 .width(viewport.width)
300 .height(viewport.height)
301 .id(wrapper_id)
302 } else {
303 node
304 }
305 })
306 .collect::<Vec<_>>();
307
308 self.runtime.absorb_registry(ctx.registry);
309 self.runtime
310 .sync_motion_declarations(&motion_declarations, self.last_snapshot.as_ref());
311 self.runtime.sync_video_nodes(&video_nodes);
312 self.runtime.sync_web_nodes(&web_nodes);
313
314 if portals.is_empty() {
315 tree
316 } else {
317 fission_core::ui::Overlay {
321 id: None,
322 content: fission_core::ui::Container::new(tree)
325 .width(viewport.width)
326 .height(viewport.height)
327 .into(),
328 overlay: fission_core::ui::ZStack {
329 id: None,
330 children: portals,
331 }
332 .into(),
333 }
334 .into()
335 }
336 };
337 if trace {
338 eprintln!("[test-trace] build done");
339 }
340
341 if trace {
343 eprintln!("[test-trace] lower start");
344 }
345 let mut cx = InternalLoweringCx::new(
346 &self.env,
347 &self.runtime.runtime_state,
348 Some(&self.measurer),
349 self.last_snapshot.as_ref(),
350 );
351 let root_id = fission_core::internal::lower_widget(&node_tree, &mut cx);
352 cx.ir.root = Some(root_id);
353
354 let layout_input_nodes = build_layout_tree(&cx.ir, &self.env);
355 self.last_ir = Some(cx.ir);
356 if trace {
357 eprintln!("[test-trace] lower done nodes={}", layout_input_nodes.len());
358 }
359
360 if trace {
362 eprintln!("[test-trace] layout start");
363 }
364 self.layout_engine.update(&layout_input_nodes);
365 self.layout_engine
366 .verify_post_update(&layout_input_nodes, root_id)?;
367 let snapshot = self.layout_engine.compute_layout(
368 &layout_input_nodes,
369 root_id,
370 viewport,
371 &|id| self.runtime.runtime_state.scroll.get_offset(id),
372 )?;
373 self.last_snapshot = Some(snapshot);
374 if trace {
375 eprintln!("[test-trace] layout done");
376 }
377 } else {
378 return Ok(());
379 }
380
381 if trace {
383 eprintln!("[test-trace] render start");
384 }
385 let mut display_list =
386 DisplayList::new(LayoutRect::new(0.0, 0.0, viewport.width, viewport.height));
387
388 if let (Some(ir), Some(snapshot)) = (&self.last_ir, &self.last_snapshot) {
389 if let Some(root_id) = ir.root {
390 let scroll_map = &self.runtime.runtime_state.scroll;
391 let animation_map = &self.runtime.runtime_state.motion;
392 generate_display_list(
393 root_id,
394 ir,
395 snapshot,
396 scroll_map,
397 animation_map,
398 &mut display_list,
399 );
400 }
401 }
402
403 self.renderer.render(&display_list)?;
404 if trace {
405 eprintln!("[test-trace] render done");
406 }
407
408 Ok(())
409 }
410
411 pub fn get_last_display_list(&self) -> Option<DisplayList> {
412 self.renderer.last_display_list.lock().unwrap().clone()
413 }
414}
415
416pub fn detect_ir_cycle(ir: &CoreIR) -> Option<Vec<WidgetId>> {
417 use std::collections::HashSet;
418
419 fn dfs(
420 ir: &CoreIR,
421 node: WidgetId,
422 visited: &mut HashSet<WidgetId>,
423 stack: &mut HashSet<WidgetId>,
424 path: &mut Vec<WidgetId>,
425 ) -> Option<Vec<WidgetId>> {
426 if !visited.insert(node) {
427 return None;
428 }
429 stack.insert(node);
430 path.push(node);
431 if let Some(n) = ir.nodes.get(&node) {
432 for &child in &n.children {
433 if stack.contains(&child) {
434 if let Some(pos) = path.iter().position(|&id| id == child) {
435 return Some(path[pos..].to_vec());
436 } else {
437 return Some(vec![child]);
438 }
439 }
440 if let Some(cy) = dfs(ir, child, visited, stack, path) {
441 return Some(cy);
442 }
443 }
444 }
445 stack.remove(&node);
446 path.pop();
447 None
448 }
449
450 if let Some(root) = ir.root {
451 let mut visited = HashSet::new();
452 let mut stack = HashSet::new();
453 let mut path = Vec::new();
454 return dfs(ir, root, &mut visited, &mut stack, &mut path);
455 }
456 None
457}
458
459fn map_fill(f: &fission_ir::op::Fill) -> fission_render::Fill {
460 match f {
461 fission_ir::op::Fill::Solid(c) => fission_render::Fill::Solid(fission_render::Color {
462 r: c.r,
463 g: c.g,
464 b: c.b,
465 a: c.a,
466 }),
467 fission_ir::op::Fill::LinearGradient { start, end, stops } => {
468 fission_render::Fill::LinearGradient {
469 start: *start,
470 end: *end,
471 stops: stops
472 .iter()
473 .map(|(o, c)| {
474 (
475 *o,
476 fission_render::Color {
477 r: c.r,
478 g: c.g,
479 b: c.b,
480 a: c.a,
481 },
482 )
483 })
484 .collect(),
485 }
486 }
487 fission_ir::op::Fill::RadialGradient {
488 center,
489 radius,
490 stops,
491 } => fission_render::Fill::RadialGradient {
492 center: *center,
493 radius: *radius,
494 stops: stops
495 .iter()
496 .map(|(o, c)| {
497 (
498 *o,
499 fission_render::Color {
500 r: c.r,
501 g: c.g,
502 b: c.b,
503 a: c.a,
504 },
505 )
506 })
507 .collect(),
508 },
509 }
510}
511
512fn map_stroke(s: &fission_ir::op::Stroke) -> fission_render::Stroke {
513 fission_render::Stroke {
514 fill: map_fill(&s.fill),
515 width: s.width,
516 dash_array: s.dash_array.clone(),
517 line_cap: match s.line_cap {
518 fission_ir::op::LineCap::Butt => fission_render::LineCap::Butt,
519 fission_ir::op::LineCap::Round => fission_render::LineCap::Round,
520 fission_ir::op::LineCap::Square => fission_render::LineCap::Square,
521 },
522 line_join: match s.line_join {
523 fission_ir::op::LineJoin::Miter => fission_render::LineJoin::Miter,
524 fission_ir::op::LineJoin::Round => fission_render::LineJoin::Round,
525 fission_ir::op::LineJoin::Bevel => fission_render::LineJoin::Bevel,
526 },
527 }
528}
529
530fn generate_display_list(
531 node_id: WidgetId,
532 ir: &CoreIR,
533 snapshot: &LayoutSnapshot,
534 scroll_map: &ScrollStateMap,
535 animation_map: &fission_core::MotionStateMap,
536 list: &mut DisplayList,
537) {
538 use std::collections::HashSet;
539 let mut visited = HashSet::new();
540 generate_display_list_with_visited(
541 node_id,
542 ir,
543 snapshot,
544 scroll_map,
545 animation_map,
546 list,
547 &mut visited,
548 );
549}
550
551fn generate_display_list_with_visited(
552 node_id: WidgetId,
553 ir: &CoreIR,
554 snapshot: &LayoutSnapshot,
555 scroll_map: &ScrollStateMap,
556 animation_map: &fission_core::MotionStateMap,
557 list: &mut DisplayList,
558 visited: &mut std::collections::HashSet<WidgetId>,
559) {
560 if !visited.insert(node_id) {
561 return;
562 }
563 if let Some(geom) = snapshot.nodes.get(&node_id) {
564 if let Some(node) = ir.nodes.get(&node_id) {
565 let mut pushed_state = false;
566 let mut clip_applied = false;
567 let rect = geom.rect;
568
569 let opacity = resolve_composite_scalar(
570 node.composite.opacity.as_ref(),
571 animation_map,
572 fission_core::MotionPropertyId::Opacity,
573 );
574 let tx = resolve_composite_scalar(
575 node.composite.translate_x.as_ref(),
576 animation_map,
577 fission_core::MotionPropertyId::TranslateX,
578 )
579 .unwrap_or(0.0);
580 let ty = resolve_composite_scalar(
581 node.composite.translate_y.as_ref(),
582 animation_map,
583 fission_core::MotionPropertyId::TranslateY,
584 )
585 .unwrap_or(0.0);
586 let scale = resolve_composite_scalar(
587 node.composite.scale.as_ref(),
588 animation_map,
589 fission_core::MotionPropertyId::Scale,
590 )
591 .unwrap_or(1.0);
592 let rotation = resolve_composite_scalar(
593 node.composite.rotation.as_ref(),
594 animation_map,
595 fission_core::MotionPropertyId::Rotation,
596 )
597 .unwrap_or(0.0);
598
599 match &node.op {
600 fission_ir::Op::Layout(fission_ir::LayoutOp::Scroll { direction, .. }) => {
601 let offset = scroll_map.get_offset(node_id);
602 list.push(DisplayOp::Save);
603 list.push(DisplayOp::ClipRect(rect));
604 clip_applied = true;
605 match direction {
606 fission_ir::FlexDirection::Row => {
607 list.push(DisplayOp::Translate(LayoutPoint::new(-offset, 0.0)));
608 }
609 fission_ir::FlexDirection::Column => {
610 list.push(DisplayOp::Translate(LayoutPoint::new(0.0, -offset)));
611 }
612 }
613 pushed_state = true;
614 }
615 fission_ir::Op::Layout(fission_ir::LayoutOp::Clip { .. }) => {
616 list.push(DisplayOp::Save);
617 list.push(DisplayOp::ClipRect(rect));
618 clip_applied = true;
619 pushed_state = true;
620 }
621 fission_ir::Op::Layout(fission_ir::LayoutOp::Transform { transform }) => {
622 list.push(DisplayOp::Save);
623 list.push(DisplayOp::Transform(*transform));
624 pushed_state = true;
625 }
626 _ => {}
627 }
628
629 if node.composite.clip_to_bounds && !clip_applied {
630 if !pushed_state {
631 list.push(DisplayOp::Save);
632 pushed_state = true;
633 }
634 list.push(DisplayOp::ClipRect(rect));
635 }
636
637 if let Some(opacity) = opacity {
638 if (opacity - 1.0).abs() > 0.001 {
639 if !pushed_state {
640 list.push(DisplayOp::Save);
641 pushed_state = true;
642 }
643 list.push(DisplayOp::OpacityLayer {
644 alpha: opacity,
645 bounds: rect,
646 });
647 }
648 }
649
650 if tx.abs() > 0.001
651 || ty.abs() > 0.001
652 || (scale - 1.0).abs() > 0.001
653 || rotation.abs() > 0.001
654 {
655 if !pushed_state {
656 list.push(DisplayOp::Save);
657 pushed_state = true;
658 }
659 list.push(DisplayOp::Transform(composite_transform_matrix(
660 rect, tx, ty, scale, rotation,
661 )));
662 }
663
664 match &node.op {
665 fission_ir::Op::Paint(fission_ir::PaintOp::DrawRect {
666 fill,
667 stroke,
668 corner_radius,
669 shadow,
670 }) => {
671 list.push(DisplayOp::DrawRect {
672 rect: geom.rect,
673 fill: fill.as_ref().map(map_fill),
674 stroke: stroke.as_ref().map(map_stroke),
675 corner_radius: *corner_radius,
676 shadow: shadow.map(|s| BoxShadow {
677 color: Color {
678 r: s.color.r,
679 g: s.color.g,
680 b: s.color.b,
681 a: s.color.a,
682 },
683 blur_radius: s.blur_radius,
684 offset: s.offset,
685 }),
686 bounds: geom.rect,
687 node_id: Some(node_id),
688 });
689 }
690 fission_ir::Op::Paint(fission_ir::PaintOp::DrawText {
691 text,
692 size,
693 color,
694 underline,
695 wrap,
696 caret_index,
697 caret_color,
698 caret_width,
699 caret_height,
700 caret_radius,
701 paragraph_style,
702 }) => {
703 list.push(DisplayOp::DrawText {
704 text: text.clone(),
705 position: LayoutPoint::new(geom.rect.x(), geom.rect.y()),
706 size: *size,
707 color: fission_render::Color {
708 r: color.r,
709 g: color.g,
710 b: color.b,
711 a: color.a,
712 },
713 bounds: geom.rect,
714 node_id: Some(node_id),
715 underline: *underline,
716 wrap: *wrap,
717 caret_index: *caret_index,
718 caret_color: caret_color.map(|color| fission_render::Color {
719 r: color.r,
720 g: color.g,
721 b: color.b,
722 a: color.a,
723 }),
724 caret_width: *caret_width,
725 caret_height: *caret_height,
726 caret_radius: *caret_radius,
727 paragraph_style: *paragraph_style,
728 });
729 }
730 fission_ir::Op::Paint(fission_ir::PaintOp::DrawRichText {
731 runs,
732 wrap,
733 caret_index,
734 caret_color,
735 caret_width,
736 caret_height,
737 caret_radius,
738 paragraph_style,
739 }) => {
740 let annotations = ir
741 .custom_render_objects
742 .get(&node_id)
743 .and_then(|sidecar| {
744 sidecar.downcast_ref::<Vec<fission_ir::op::RichTextAnnotation>>()
745 })
746 .cloned()
747 .unwrap_or_default();
748 list.push(DisplayOp::DrawRichText {
749 runs: runs
750 .iter()
751 .map(|r| fission_render::TextRun {
752 text: r.text.clone(),
753 style: fission_render::TextStyle {
754 font_size: r.style.font_size,
755 color: fission_render::Color {
756 r: r.style.color.r,
757 g: r.style.color.g,
758 b: r.style.color.b,
759 a: r.style.color.a,
760 },
761 underline: r.style.underline,
762 font_family: r.style.font_family.clone(),
763 locale: r.style.locale.clone(),
764 font_weight: r.style.font_weight,
765 font_style: r.style.font_style,
766 line_height: r.style.line_height,
767 letter_spacing: r.style.letter_spacing,
768 background_color: r.style.background_color.map(|c| {
769 fission_render::Color {
770 r: c.r,
771 g: c.g,
772 b: c.b,
773 a: c.a,
774 }
775 }),
776 },
777 })
778 .collect(),
779 position: LayoutPoint::new(geom.rect.x(), geom.rect.y()),
780 bounds: geom.rect,
781 node_id: Some(node_id),
782 wrap: *wrap,
783 caret_index: *caret_index,
784 caret_color: caret_color.map(|color| fission_render::Color {
785 r: color.r,
786 g: color.g,
787 b: color.b,
788 a: color.a,
789 }),
790 caret_width: *caret_width,
791 caret_height: *caret_height,
792 caret_radius: *caret_radius,
793 paragraph_style: *paragraph_style,
794 annotations,
795 });
796 }
797 fission_ir::Op::Paint(fission_ir::PaintOp::DrawSvg {
798 content,
799 fill,
800 stroke,
801 }) => {
802 list.push(DisplayOp::DrawSvg {
803 content: content.clone(),
804 fill: fill.as_ref().map(map_fill),
805 stroke: stroke.as_ref().map(map_stroke),
806 bounds: geom.rect,
807 node_id: Some(node_id),
808 });
809 }
810 fission_ir::Op::Paint(fission_ir::PaintOp::DrawImage {
811 request,
812 fit,
813 alignment,
814 }) => {
815 list.push(DisplayOp::DrawImage {
816 rect: geom.rect,
817 request: request.clone(),
818 fit: match fit {
819 fission_ir::op::ImageFit::Contain => fission_render::ImageFit::Contain,
820 fission_ir::op::ImageFit::Cover => fission_render::ImageFit::Cover,
821 fission_ir::op::ImageFit::Fill => fission_render::ImageFit::Fill,
822 fission_ir::op::ImageFit::None => fission_render::ImageFit::None,
823 },
824 alignment: *alignment,
825 bounds: geom.rect,
826 node_id: Some(node_id),
827 });
828 }
829 fission_ir::Op::Paint(fission_ir::PaintOp::DrawPath { path, fill, stroke }) => {
830 list.push(DisplayOp::DrawPath {
831 path: path.clone(),
832 fill: fill.as_ref().map(map_fill),
833 stroke: stroke.as_ref().map(map_stroke),
834 bounds: geom.rect,
835 node_id: Some(node_id),
836 });
837 }
838 fission_ir::Op::Layout(fission_ir::LayoutOp::Embed {
839 kind, widget_id, ..
840 }) => {
841 list.push(DisplayOp::DrawSurface {
842 rect: geom.rect,
843 surface_id: embed_surface_id(kind, *widget_id),
844 position: 0,
845 bounds: geom.rect,
846 node_id: Some(node_id),
847 });
848 }
849 _ => {}
850 }
851
852 for child in &node.children {
853 generate_display_list_with_visited(
854 *child,
855 ir,
856 snapshot,
857 scroll_map,
858 animation_map,
859 list,
860 visited,
861 );
862 }
863
864 if pushed_state {
865 list.push(DisplayOp::Restore);
866 }
867 }
868 }
869}
870
871fn resolve_composite_scalar(
872 scalar: Option<&fission_ir::CompositeScalar>,
873 animation_map: &fission_core::MotionStateMap,
874 property: fission_core::MotionPropertyId,
875) -> Option<f32> {
876 let scalar = scalar?;
877 Some(
878 scalar
879 .motion_target
880 .map(|target| animation_map.scalar_value(target, property))
881 .unwrap_or(scalar.base),
882 )
883}
884
885fn composite_transform_matrix(
886 rect: LayoutRect,
887 translate_x: f32,
888 translate_y: f32,
889 scale: f32,
890 rotation: f32,
891) -> [f32; 16] {
892 let center_x = rect.origin.x + rect.size.width * 0.5;
893 let center_y = rect.origin.y + rect.size.height * 0.5;
894
895 let to_center = translation_matrix(center_x, center_y);
896 let from_center = translation_matrix(-center_x, -center_y);
897 let scale_matrix = scale_matrix(scale);
898 let rotation_matrix = rotation_z_matrix(rotation);
899 let motion_translate = translation_matrix(translate_x, translate_y);
900
901 multiply_matrix(
902 motion_translate,
903 multiply_matrix(
904 to_center,
905 multiply_matrix(rotation_matrix, multiply_matrix(scale_matrix, from_center)),
906 ),
907 )
908}
909
910fn translation_matrix(tx: f32, ty: f32) -> [f32; 16] {
911 [
912 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, tx, ty, 0.0, 1.0,
913 ]
914}
915
916fn scale_matrix(scale: f32) -> [f32; 16] {
917 [
918 scale, 0.0, 0.0, 0.0, 0.0, scale, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
919 ]
920}
921
922fn rotation_z_matrix(radians: f32) -> [f32; 16] {
923 let sin = radians.sin();
924 let cos = radians.cos();
925 [
926 cos, sin, 0.0, 0.0, -sin, cos, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
927 ]
928}
929
930fn multiply_matrix(a: [f32; 16], b: [f32; 16]) -> [f32; 16] {
931 let mut out = [0.0; 16];
932 for row in 0..4 {
933 for col in 0..4 {
934 let mut sum = 0.0;
935 for k in 0..4 {
936 sum += a[row * 4 + k] * b[k * 4 + col];
937 }
938 out[row * 4 + col] = sum;
939 }
940 }
941 out
942}