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 animation_requests = ctx.take_animation_requests();
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 for (target, request) in animation_requests {
310 self.runtime.enqueue_animation(target, request);
311 }
312 self.runtime.sync_video_nodes(&video_nodes);
313 self.runtime.sync_web_nodes(&web_nodes);
314
315 if portals.is_empty() {
316 tree
317 } else {
318 fission_core::ui::Overlay {
322 id: None,
323 content: fission_core::ui::Container::new(tree)
326 .width(viewport.width)
327 .height(viewport.height)
328 .into(),
329 overlay: fission_core::ui::ZStack {
330 id: None,
331 children: portals,
332 }
333 .into(),
334 }
335 .into()
336 }
337 };
338 if trace {
339 eprintln!("[test-trace] build done");
340 }
341
342 if trace {
344 eprintln!("[test-trace] lower start");
345 }
346 let mut cx = InternalLoweringCx::new(
347 &self.env,
348 &self.runtime.runtime_state,
349 Some(&self.measurer),
350 self.last_snapshot.as_ref(),
351 );
352 let root_id = fission_core::internal::lower_widget(&node_tree, &mut cx);
353 cx.ir.root = Some(root_id);
354
355 let layout_input_nodes = build_layout_tree(&cx.ir, &self.env);
356 self.last_ir = Some(cx.ir);
357 if trace {
358 eprintln!("[test-trace] lower done nodes={}", layout_input_nodes.len());
359 }
360
361 if trace {
363 eprintln!("[test-trace] layout start");
364 }
365 self.layout_engine.update(&layout_input_nodes);
366 self.layout_engine
367 .verify_post_update(&layout_input_nodes, root_id)?;
368 let snapshot = self.layout_engine.compute_layout(
369 &layout_input_nodes,
370 root_id,
371 viewport,
372 &|id| self.runtime.runtime_state.scroll.get_offset(id),
373 )?;
374 self.last_snapshot = Some(snapshot);
375 if trace {
376 eprintln!("[test-trace] layout done");
377 }
378 } else {
379 return Ok(());
380 }
381
382 if trace {
384 eprintln!("[test-trace] render start");
385 }
386 let mut display_list =
387 DisplayList::new(LayoutRect::new(0.0, 0.0, viewport.width, viewport.height));
388
389 if let (Some(ir), Some(snapshot)) = (&self.last_ir, &self.last_snapshot) {
390 if let Some(root_id) = ir.root {
391 let scroll_map = &self.runtime.runtime_state.scroll;
392 let animation_map = &self.runtime.runtime_state.animation;
393 generate_display_list(
394 root_id,
395 ir,
396 snapshot,
397 scroll_map,
398 animation_map,
399 &mut display_list,
400 );
401 }
402 }
403
404 self.renderer.render(&display_list)?;
405 if trace {
406 eprintln!("[test-trace] render done");
407 }
408
409 Ok(())
410 }
411
412 pub fn get_last_display_list(&self) -> Option<DisplayList> {
413 self.renderer.last_display_list.lock().unwrap().clone()
414 }
415}
416
417pub fn detect_ir_cycle(ir: &CoreIR) -> Option<Vec<WidgetId>> {
418 use std::collections::HashSet;
419
420 fn dfs(
421 ir: &CoreIR,
422 node: WidgetId,
423 visited: &mut HashSet<WidgetId>,
424 stack: &mut HashSet<WidgetId>,
425 path: &mut Vec<WidgetId>,
426 ) -> Option<Vec<WidgetId>> {
427 if !visited.insert(node) {
428 return None;
429 }
430 stack.insert(node);
431 path.push(node);
432 if let Some(n) = ir.nodes.get(&node) {
433 for &child in &n.children {
434 if stack.contains(&child) {
435 if let Some(pos) = path.iter().position(|&id| id == child) {
436 return Some(path[pos..].to_vec());
437 } else {
438 return Some(vec![child]);
439 }
440 }
441 if let Some(cy) = dfs(ir, child, visited, stack, path) {
442 return Some(cy);
443 }
444 }
445 }
446 stack.remove(&node);
447 path.pop();
448 None
449 }
450
451 if let Some(root) = ir.root {
452 let mut visited = HashSet::new();
453 let mut stack = HashSet::new();
454 let mut path = Vec::new();
455 return dfs(ir, root, &mut visited, &mut stack, &mut path);
456 }
457 None
458}
459
460fn map_fill(f: &fission_ir::op::Fill) -> fission_render::Fill {
461 match f {
462 fission_ir::op::Fill::Solid(c) => fission_render::Fill::Solid(fission_render::Color {
463 r: c.r,
464 g: c.g,
465 b: c.b,
466 a: c.a,
467 }),
468 fission_ir::op::Fill::LinearGradient { start, end, stops } => {
469 fission_render::Fill::LinearGradient {
470 start: *start,
471 end: *end,
472 stops: stops
473 .iter()
474 .map(|(o, c)| {
475 (
476 *o,
477 fission_render::Color {
478 r: c.r,
479 g: c.g,
480 b: c.b,
481 a: c.a,
482 },
483 )
484 })
485 .collect(),
486 }
487 }
488 fission_ir::op::Fill::RadialGradient {
489 center,
490 radius,
491 stops,
492 } => fission_render::Fill::RadialGradient {
493 center: *center,
494 radius: *radius,
495 stops: stops
496 .iter()
497 .map(|(o, c)| {
498 (
499 *o,
500 fission_render::Color {
501 r: c.r,
502 g: c.g,
503 b: c.b,
504 a: c.a,
505 },
506 )
507 })
508 .collect(),
509 },
510 }
511}
512
513fn map_stroke(s: &fission_ir::op::Stroke) -> fission_render::Stroke {
514 fission_render::Stroke {
515 fill: map_fill(&s.fill),
516 width: s.width,
517 dash_array: s.dash_array.clone(),
518 line_cap: match s.line_cap {
519 fission_ir::op::LineCap::Butt => fission_render::LineCap::Butt,
520 fission_ir::op::LineCap::Round => fission_render::LineCap::Round,
521 fission_ir::op::LineCap::Square => fission_render::LineCap::Square,
522 },
523 line_join: match s.line_join {
524 fission_ir::op::LineJoin::Miter => fission_render::LineJoin::Miter,
525 fission_ir::op::LineJoin::Round => fission_render::LineJoin::Round,
526 fission_ir::op::LineJoin::Bevel => fission_render::LineJoin::Bevel,
527 },
528 }
529}
530
531fn generate_display_list(
532 node_id: WidgetId,
533 ir: &CoreIR,
534 snapshot: &LayoutSnapshot,
535 scroll_map: &ScrollStateMap,
536 animation_map: &fission_core::env::AnimationStateMap,
537 list: &mut DisplayList,
538) {
539 use std::collections::HashSet;
540 let mut visited = HashSet::new();
541 generate_display_list_with_visited(
542 node_id,
543 ir,
544 snapshot,
545 scroll_map,
546 animation_map,
547 list,
548 &mut visited,
549 );
550}
551
552fn generate_display_list_with_visited(
553 node_id: WidgetId,
554 ir: &CoreIR,
555 snapshot: &LayoutSnapshot,
556 scroll_map: &ScrollStateMap,
557 animation_map: &fission_core::env::AnimationStateMap,
558 list: &mut DisplayList,
559 visited: &mut std::collections::HashSet<WidgetId>,
560) {
561 if !visited.insert(node_id) {
562 return;
563 }
564 if let Some(geom) = snapshot.nodes.get(&node_id) {
565 if let Some(node) = ir.nodes.get(&node_id) {
566 let mut pushed_state = false;
567 let mut clip_applied = false;
568 let rect = geom.rect;
569
570 let opacity = resolve_composite_scalar(
571 node.composite.opacity.as_ref(),
572 animation_map,
573 fission_core::registry::AnimationPropertyId::Opacity,
574 );
575 let tx = resolve_composite_scalar(
576 node.composite.translate_x.as_ref(),
577 animation_map,
578 fission_core::registry::AnimationPropertyId::TranslateX,
579 )
580 .unwrap_or(0.0);
581 let ty = resolve_composite_scalar(
582 node.composite.translate_y.as_ref(),
583 animation_map,
584 fission_core::registry::AnimationPropertyId::TranslateY,
585 )
586 .unwrap_or(0.0);
587 let scale = resolve_composite_scalar(
588 node.composite.scale.as_ref(),
589 animation_map,
590 fission_core::registry::AnimationPropertyId::Scale,
591 )
592 .unwrap_or(1.0);
593 let rotation = resolve_composite_scalar(
594 node.composite.rotation.as_ref(),
595 animation_map,
596 fission_core::registry::AnimationPropertyId::Rotation,
597 )
598 .unwrap_or(0.0);
599
600 match &node.op {
601 fission_ir::Op::Layout(fission_ir::LayoutOp::Scroll { direction, .. }) => {
602 let offset = scroll_map.get_offset(node_id);
603 list.push(DisplayOp::Save);
604 list.push(DisplayOp::ClipRect(rect));
605 clip_applied = true;
606 match direction {
607 fission_ir::FlexDirection::Row => {
608 list.push(DisplayOp::Translate(LayoutPoint::new(-offset, 0.0)));
609 }
610 fission_ir::FlexDirection::Column => {
611 list.push(DisplayOp::Translate(LayoutPoint::new(0.0, -offset)));
612 }
613 }
614 pushed_state = true;
615 }
616 fission_ir::Op::Layout(fission_ir::LayoutOp::Clip { .. }) => {
617 list.push(DisplayOp::Save);
618 list.push(DisplayOp::ClipRect(rect));
619 clip_applied = true;
620 pushed_state = true;
621 }
622 fission_ir::Op::Layout(fission_ir::LayoutOp::Transform { transform }) => {
623 list.push(DisplayOp::Save);
624 list.push(DisplayOp::Transform(*transform));
625 pushed_state = true;
626 }
627 _ => {}
628 }
629
630 if node.composite.clip_to_bounds && !clip_applied {
631 if !pushed_state {
632 list.push(DisplayOp::Save);
633 pushed_state = true;
634 }
635 list.push(DisplayOp::ClipRect(rect));
636 }
637
638 if let Some(opacity) = opacity {
639 if (opacity - 1.0).abs() > 0.001 {
640 if !pushed_state {
641 list.push(DisplayOp::Save);
642 pushed_state = true;
643 }
644 list.push(DisplayOp::OpacityLayer {
645 alpha: opacity,
646 bounds: rect,
647 });
648 }
649 }
650
651 if tx.abs() > 0.001
652 || ty.abs() > 0.001
653 || (scale - 1.0).abs() > 0.001
654 || rotation.abs() > 0.001
655 {
656 if !pushed_state {
657 list.push(DisplayOp::Save);
658 pushed_state = true;
659 }
660 list.push(DisplayOp::Transform(composite_transform_matrix(
661 rect, tx, ty, scale, rotation,
662 )));
663 }
664
665 match &node.op {
666 fission_ir::Op::Paint(fission_ir::PaintOp::DrawRect {
667 fill,
668 stroke,
669 corner_radius,
670 shadow,
671 }) => {
672 list.push(DisplayOp::DrawRect {
673 rect: geom.rect,
674 fill: fill.as_ref().map(map_fill),
675 stroke: stroke.as_ref().map(map_stroke),
676 corner_radius: *corner_radius,
677 shadow: shadow.map(|s| BoxShadow {
678 color: Color {
679 r: s.color.r,
680 g: s.color.g,
681 b: s.color.b,
682 a: s.color.a,
683 },
684 blur_radius: s.blur_radius,
685 offset: s.offset,
686 }),
687 bounds: geom.rect,
688 node_id: Some(node_id),
689 });
690 }
691 fission_ir::Op::Paint(fission_ir::PaintOp::DrawText {
692 text,
693 size,
694 color,
695 underline,
696 wrap,
697 caret_index,
698 caret_color,
699 caret_width,
700 caret_height,
701 caret_radius,
702 paragraph_style,
703 }) => {
704 list.push(DisplayOp::DrawText {
705 text: text.clone(),
706 position: LayoutPoint::new(geom.rect.x(), geom.rect.y()),
707 size: *size,
708 color: fission_render::Color {
709 r: color.r,
710 g: color.g,
711 b: color.b,
712 a: color.a,
713 },
714 bounds: geom.rect,
715 node_id: Some(node_id),
716 underline: *underline,
717 wrap: *wrap,
718 caret_index: *caret_index,
719 caret_color: caret_color.map(|color| fission_render::Color {
720 r: color.r,
721 g: color.g,
722 b: color.b,
723 a: color.a,
724 }),
725 caret_width: *caret_width,
726 caret_height: *caret_height,
727 caret_radius: *caret_radius,
728 paragraph_style: *paragraph_style,
729 });
730 }
731 fission_ir::Op::Paint(fission_ir::PaintOp::DrawRichText {
732 runs,
733 wrap,
734 caret_index,
735 caret_color,
736 caret_width,
737 caret_height,
738 caret_radius,
739 paragraph_style,
740 }) => {
741 let annotations = ir
742 .custom_render_objects
743 .get(&node_id)
744 .and_then(|sidecar| {
745 sidecar.downcast_ref::<Vec<fission_ir::op::RichTextAnnotation>>()
746 })
747 .cloned()
748 .unwrap_or_default();
749 list.push(DisplayOp::DrawRichText {
750 runs: runs
751 .iter()
752 .map(|r| fission_render::TextRun {
753 text: r.text.clone(),
754 style: fission_render::TextStyle {
755 font_size: r.style.font_size,
756 color: fission_render::Color {
757 r: r.style.color.r,
758 g: r.style.color.g,
759 b: r.style.color.b,
760 a: r.style.color.a,
761 },
762 underline: r.style.underline,
763 font_family: r.style.font_family.clone(),
764 locale: r.style.locale.clone(),
765 font_weight: r.style.font_weight,
766 font_style: r.style.font_style,
767 line_height: r.style.line_height,
768 letter_spacing: r.style.letter_spacing,
769 background_color: r.style.background_color.map(|c| {
770 fission_render::Color {
771 r: c.r,
772 g: c.g,
773 b: c.b,
774 a: c.a,
775 }
776 }),
777 },
778 })
779 .collect(),
780 position: LayoutPoint::new(geom.rect.x(), geom.rect.y()),
781 bounds: geom.rect,
782 node_id: Some(node_id),
783 wrap: *wrap,
784 caret_index: *caret_index,
785 caret_color: caret_color.map(|color| fission_render::Color {
786 r: color.r,
787 g: color.g,
788 b: color.b,
789 a: color.a,
790 }),
791 caret_width: *caret_width,
792 caret_height: *caret_height,
793 caret_radius: *caret_radius,
794 paragraph_style: *paragraph_style,
795 annotations,
796 });
797 }
798 fission_ir::Op::Paint(fission_ir::PaintOp::DrawSvg {
799 content,
800 fill,
801 stroke,
802 }) => {
803 list.push(DisplayOp::DrawSvg {
804 content: content.clone(),
805 fill: fill.as_ref().map(map_fill),
806 stroke: stroke.as_ref().map(map_stroke),
807 bounds: geom.rect,
808 node_id: Some(node_id),
809 });
810 }
811 fission_ir::Op::Paint(fission_ir::PaintOp::DrawImage {
812 request,
813 fit,
814 alignment,
815 }) => {
816 list.push(DisplayOp::DrawImage {
817 rect: geom.rect,
818 request: request.clone(),
819 fit: match fit {
820 fission_ir::op::ImageFit::Contain => fission_render::ImageFit::Contain,
821 fission_ir::op::ImageFit::Cover => fission_render::ImageFit::Cover,
822 fission_ir::op::ImageFit::Fill => fission_render::ImageFit::Fill,
823 fission_ir::op::ImageFit::None => fission_render::ImageFit::None,
824 },
825 alignment: *alignment,
826 bounds: geom.rect,
827 node_id: Some(node_id),
828 });
829 }
830 fission_ir::Op::Paint(fission_ir::PaintOp::DrawPath { path, fill, stroke }) => {
831 list.push(DisplayOp::DrawPath {
832 path: path.clone(),
833 fill: fill.as_ref().map(map_fill),
834 stroke: stroke.as_ref().map(map_stroke),
835 bounds: geom.rect,
836 node_id: Some(node_id),
837 });
838 }
839 fission_ir::Op::Layout(fission_ir::LayoutOp::Embed {
840 kind, widget_id, ..
841 }) => {
842 list.push(DisplayOp::DrawSurface {
843 rect: geom.rect,
844 surface_id: embed_surface_id(kind, *widget_id),
845 position: 0,
846 bounds: geom.rect,
847 node_id: Some(node_id),
848 });
849 }
850 _ => {}
851 }
852
853 for child in &node.children {
854 generate_display_list_with_visited(
855 *child,
856 ir,
857 snapshot,
858 scroll_map,
859 animation_map,
860 list,
861 visited,
862 );
863 }
864
865 if pushed_state {
866 list.push(DisplayOp::Restore);
867 }
868 }
869 }
870}
871
872fn resolve_composite_scalar(
873 scalar: Option<&fission_ir::CompositeScalar>,
874 animation_map: &fission_core::env::AnimationStateMap,
875 property: fission_core::registry::AnimationPropertyId,
876) -> Option<f32> {
877 let scalar = scalar?;
878 Some(
879 scalar
880 .animation_target
881 .and_then(|target| animation_map.values.get(&(target, property)).copied())
882 .unwrap_or(scalar.base),
883 )
884}
885
886fn composite_transform_matrix(
887 rect: LayoutRect,
888 translate_x: f32,
889 translate_y: f32,
890 scale: f32,
891 rotation: f32,
892) -> [f32; 16] {
893 let center_x = rect.origin.x + rect.size.width * 0.5;
894 let center_y = rect.origin.y + rect.size.height * 0.5;
895
896 let to_center = translation_matrix(center_x, center_y);
897 let from_center = translation_matrix(-center_x, -center_y);
898 let scale_matrix = scale_matrix(scale);
899 let rotation_matrix = rotation_z_matrix(rotation);
900 let animated_translate = translation_matrix(translate_x, translate_y);
901
902 multiply_matrix(
903 animated_translate,
904 multiply_matrix(
905 to_center,
906 multiply_matrix(rotation_matrix, multiply_matrix(scale_matrix, from_center)),
907 ),
908 )
909}
910
911fn translation_matrix(tx: f32, ty: f32) -> [f32; 16] {
912 [
913 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,
914 ]
915}
916
917fn scale_matrix(scale: f32) -> [f32; 16] {
918 [
919 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,
920 ]
921}
922
923fn rotation_z_matrix(radians: f32) -> [f32; 16] {
924 let sin = radians.sin();
925 let cos = radians.cos();
926 [
927 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,
928 ]
929}
930
931fn multiply_matrix(a: [f32; 16], b: [f32; 16]) -> [f32; 16] {
932 let mut out = [0.0; 16];
933 for row in 0..4 {
934 for col in 0..4 {
935 let mut sum = 0.0;
936 for k in 0..4 {
937 sum += a[row * 4 + k] * b[k * 4 + col];
938 }
939 out[row * 4 + col] = sum;
940 }
941 }
942 out
943}