1use super::semantics::{ActionEntry, Semantics};
2use super::widget_id::WidgetNodeId;
3use crate::NodeId;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub enum Op {
10 Structural(StructuralOp),
11 Layout(LayoutOp),
12 Paint(PaintOp),
13 Semantics(Semantics),
14}
15
16impl std::hash::Hash for Op {
17 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
18 match self {
19 Self::Structural(s) => {
20 0.hash(state);
21 s.hash(state);
22 }
23 Self::Layout(l) => {
24 1.hash(state);
25 l.hash(state);
26 }
27 Self::Paint(p) => {
28 2.hash(state);
29 p.hash(state);
30 }
31 Self::Semantics(s) => {
32 3.hash(state);
33 s.hash(state);
34 }
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
40pub enum StructuralOp {
41 Group { stable_hash: u64 },
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
45pub struct CompositeScalar {
46 pub base: f32,
47 pub animation_target: Option<WidgetNodeId>,
48}
49
50impl CompositeScalar {
51 pub fn new(base: f32) -> Self {
52 Self {
53 base,
54 animation_target: None,
55 }
56 }
57
58 pub fn animated(mut self, target: WidgetNodeId) -> Self {
59 self.animation_target = Some(target);
60 self
61 }
62}
63
64impl std::hash::Hash for CompositeScalar {
65 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
66 self.base.to_bits().hash(state);
67 self.animation_target.hash(state);
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash, Default)]
72pub struct CompositeStyle {
73 pub opacity: Option<CompositeScalar>,
74 pub translate_x: Option<CompositeScalar>,
75 pub translate_y: Option<CompositeScalar>,
76 pub scale: Option<CompositeScalar>,
77 pub rotation: Option<CompositeScalar>,
78 pub clip_to_bounds: bool,
79 pub repaint_boundary: bool,
80}
81
82pub type LayoutUnit = f32;
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
85pub enum TextAlign {
86 Left,
87 Right,
88 Center,
89 Justify,
90 #[default]
91 Start,
92 End,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
96pub enum TextOverflow {
97 Clip,
98 Ellipsis,
99 Fade,
100 #[default]
101 Visible,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
105pub enum TextDirection {
106 #[default]
107 Auto,
108 Ltr,
109 Rtl,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
113pub enum TextWidthBasis {
114 #[default]
115 Parent,
116 LongestLine,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
120pub enum MouseCursor {
121 #[default]
122 Basic,
123 Pointer,
124 Text,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
128pub struct TextHeightBehavior {
129 pub apply_height_to_first_ascent: bool,
130 pub apply_height_to_last_descent: bool,
131}
132
133impl Default for TextHeightBehavior {
134 fn default() -> Self {
135 Self {
136 apply_height_to_first_ascent: true,
137 apply_height_to_last_descent: true,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
143pub struct TextParagraphStyle {
144 pub text_align: TextAlign,
145 pub max_lines: Option<usize>,
146 pub overflow: TextOverflow,
147 #[serde(default)]
148 pub text_direction: TextDirection,
149 #[serde(default)]
150 pub text_width_basis: TextWidthBasis,
151 #[serde(default)]
152 pub strut_line_height: Option<LayoutUnit>,
153 #[serde(default)]
154 pub text_height_behavior: TextHeightBehavior,
155}
156
157impl PartialEq for TextParagraphStyle {
158 fn eq(&self, other: &Self) -> bool {
159 self.text_align == other.text_align
160 && self.max_lines == other.max_lines
161 && self.overflow == other.overflow
162 && self.text_direction == other.text_direction
163 && self.text_width_basis == other.text_width_basis
164 && self.strut_line_height.map(f32::to_bits) == other.strut_line_height.map(f32::to_bits)
165 && self.text_height_behavior == other.text_height_behavior
166 }
167}
168
169impl Eq for TextParagraphStyle {}
170
171impl std::hash::Hash for TextParagraphStyle {
172 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
173 self.text_align.hash(state);
174 self.max_lines.hash(state);
175 self.overflow.hash(state);
176 self.text_direction.hash(state);
177 self.text_width_basis.hash(state);
178 self.strut_line_height.map(f32::to_bits).hash(state);
179 self.text_height_behavior.hash(state);
180 }
181}
182
183const TEXT_PARAGRAPH_ALIGN_BITS: u32 = 0b111;
184const TEXT_PARAGRAPH_OVERFLOW_BITS: u32 = 0b111 << 3;
185const TEXT_PARAGRAPH_MAX_LINES_SHIFT: u32 = 6;
186const TEXT_PARAGRAPH_SENTINEL: u32 = 1;
187const TEXT_PARAGRAPH_MAX_ENCODED_LINES: usize = ((1 << 24) - 1) >> TEXT_PARAGRAPH_MAX_LINES_SHIFT;
188
189const fn text_align_code(align: TextAlign) -> u32 {
190 match align {
191 TextAlign::Start => 0,
192 TextAlign::Left => 1,
193 TextAlign::Center => 2,
194 TextAlign::Right => 3,
195 TextAlign::End => 4,
196 TextAlign::Justify => 5,
197 }
198}
199
200const fn text_overflow_code(overflow: TextOverflow) -> u32 {
201 match overflow {
202 TextOverflow::Visible => 0,
203 TextOverflow::Clip => 1,
204 TextOverflow::Ellipsis => 2,
205 TextOverflow::Fade => 3,
206 }
207}
208
209const fn decode_text_align(code: u32) -> TextAlign {
210 match code {
211 1 => TextAlign::Left,
212 2 => TextAlign::Center,
213 3 => TextAlign::Right,
214 4 => TextAlign::End,
215 5 => TextAlign::Justify,
216 _ => TextAlign::Start,
217 }
218}
219
220const fn decode_text_overflow(code: u32) -> TextOverflow {
221 match code {
222 1 => TextOverflow::Clip,
223 2 => TextOverflow::Ellipsis,
224 3 => TextOverflow::Fade,
225 _ => TextOverflow::Visible,
226 }
227}
228
229pub fn encode_text_paragraph_style(style: TextParagraphStyle) -> Option<LayoutUnit> {
230 if style == TextParagraphStyle::default() {
231 return None;
232 }
233 if style.text_direction != TextDirection::Auto
234 || style.text_width_basis != TextWidthBasis::Parent
235 || style.strut_line_height.is_some()
236 || style.text_height_behavior != TextHeightBehavior::default()
237 {
238 return None;
239 }
240
241 let max_lines = style
242 .max_lines
243 .unwrap_or(0)
244 .min(TEXT_PARAGRAPH_MAX_ENCODED_LINES) as u32;
245 let encoded = TEXT_PARAGRAPH_SENTINEL
246 + text_align_code(style.text_align)
247 + (text_overflow_code(style.overflow) << 3)
248 + (max_lines << TEXT_PARAGRAPH_MAX_LINES_SHIFT);
249
250 Some(-(encoded as LayoutUnit))
251}
252
253pub fn decode_text_paragraph_style(
254 encoded_width: Option<LayoutUnit>,
255) -> Option<TextParagraphStyle> {
256 let encoded_width = encoded_width?;
257 if !encoded_width.is_finite() || encoded_width >= 0.0 {
258 return None;
259 }
260
261 let raw = (-encoded_width).round();
262 if raw < TEXT_PARAGRAPH_SENTINEL as f32 {
263 return None;
264 }
265
266 let bits = raw as u32 - TEXT_PARAGRAPH_SENTINEL;
267 let text_align = decode_text_align(bits & TEXT_PARAGRAPH_ALIGN_BITS);
268 let overflow = decode_text_overflow((bits & TEXT_PARAGRAPH_OVERFLOW_BITS) >> 3);
269 let max_lines = match bits >> TEXT_PARAGRAPH_MAX_LINES_SHIFT {
270 0 => None,
271 lines => Some(lines as usize),
272 };
273
274 Some(TextParagraphStyle {
275 text_align,
276 max_lines,
277 overflow,
278 text_direction: TextDirection::Auto,
279 text_width_basis: TextWidthBasis::Parent,
280 strut_line_height: None,
281 text_height_behavior: TextHeightBehavior::default(),
282 })
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
286pub enum FlexDirection {
287 Row,
288 Column,
289}
290
291impl Default for FlexDirection {
292 fn default() -> Self {
293 FlexDirection::Row
294 }
295}
296
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
298pub enum EmbedKind {
299 Video,
300 Web,
301 Custom(Vec<u8>),
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
305pub enum GridTrack {
306 Points(LayoutUnit),
307 Percent(f32),
308 Fr(f32),
309 Auto,
310 MinContent,
311 MaxContent,
312}
313
314impl std::hash::Hash for GridTrack {
315 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
316 match self {
317 Self::Points(u) => {
318 0.hash(state);
319 u.to_bits().hash(state);
320 }
321 Self::Percent(f) => {
322 1.hash(state);
323 f.to_bits().hash(state);
324 }
325 Self::Fr(f) => {
326 2.hash(state);
327 f.to_bits().hash(state);
328 }
329 Self::Auto => {
330 3.hash(state);
331 }
332 Self::MinContent => {
333 4.hash(state);
334 }
335 Self::MaxContent => {
336 5.hash(state);
337 }
338 }
339 }
340}
341
342#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
343pub enum GridPlacement {
344 Auto,
345 Line(i16),
346 Span(u16),
347}
348
349impl Default for GridPlacement {
350 fn default() -> Self {
351 Self::Auto
352 }
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
356pub enum FlexWrap {
357 NoWrap,
358 Wrap,
359 WrapReverse,
360}
361
362impl Default for FlexWrap {
363 fn default() -> Self {
364 FlexWrap::NoWrap
365 }
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
369pub enum AlignItems {
370 Start,
371 End,
372 Center,
373 Stretch,
374 Baseline,
375}
376
377impl Default for AlignItems {
378 fn default() -> Self {
379 AlignItems::Stretch
380 }
381}
382
383#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
384pub enum JustifyContent {
385 Start,
386 End,
387 Center,
388 SpaceBetween,
389 SpaceAround,
390 SpaceEvenly,
391}
392
393impl Default for JustifyContent {
394 fn default() -> Self {
395 JustifyContent::Start
396 }
397}
398
399#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
400pub enum LayoutOp {
401 Box {
402 width: Option<LayoutUnit>,
403 height: Option<LayoutUnit>,
404 min_width: Option<LayoutUnit>,
405 max_width: Option<LayoutUnit>,
406 min_height: Option<LayoutUnit>,
407 max_height: Option<LayoutUnit>,
408 padding: [LayoutUnit; 4],
409 flex_grow: LayoutUnit,
410 flex_shrink: LayoutUnit,
411 aspect_ratio: Option<f32>,
412 },
413 Flex {
414 direction: FlexDirection,
415 wrap: FlexWrap,
416 flex_grow: LayoutUnit,
417 flex_shrink: LayoutUnit,
418 padding: [LayoutUnit; 4],
419 gap: Option<LayoutUnit>,
420 align_items: AlignItems,
421 justify_content: JustifyContent,
422 },
423 Grid {
424 columns: Vec<GridTrack>,
425 rows: Vec<GridTrack>,
426 column_gap: Option<LayoutUnit>,
427 row_gap: Option<LayoutUnit>,
428 padding: [LayoutUnit; 4],
429 },
430 GridItem {
431 row_start: GridPlacement,
432 row_end: GridPlacement,
433 col_start: GridPlacement,
434 col_end: GridPlacement,
435 },
436 Scroll {
437 direction: FlexDirection,
438 show_scrollbar: bool,
439 width: Option<LayoutUnit>,
440 height: Option<LayoutUnit>,
441 min_width: Option<LayoutUnit>,
442 max_width: Option<LayoutUnit>,
443 min_height: Option<LayoutUnit>,
444 max_height: Option<LayoutUnit>,
445 padding: [LayoutUnit; 4],
446 flex_grow: LayoutUnit,
447 flex_shrink: LayoutUnit,
448 },
449 Embed {
450 kind: EmbedKind,
451 widget_id: WidgetNodeId,
452 width: Option<LayoutUnit>,
453 height: Option<LayoutUnit>,
454 },
455 AbsoluteFill,
456 Positioned {
457 left: Option<LayoutUnit>,
458 top: Option<LayoutUnit>,
459 right: Option<LayoutUnit>,
460 bottom: Option<LayoutUnit>,
461 width: Option<LayoutUnit>,
462 height: Option<LayoutUnit>,
463 },
464 ZStack,
465 Align,
466 Flyout {
467 anchor: NodeId,
468 content: NodeId,
469 },
470 Transform {
471 transform: [f32; 16],
472 },
473 Clip {
474 path: Option<String>,
475 },
476}
477
478impl std::hash::Hash for LayoutOp {
479 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
480 let hash_unit = |u: LayoutUnit, h: &mut H| u.to_bits().hash(h);
481 let hash_opt_unit = |u: Option<LayoutUnit>, h: &mut H| u.map(|v| v.to_bits()).hash(h);
482 let hash_units = |us: [LayoutUnit; 4], h: &mut H| {
483 for u in us {
484 u.to_bits().hash(h);
485 }
486 };
487
488 match self {
489 Self::Box {
490 width,
491 height,
492 min_width,
493 max_width,
494 min_height,
495 max_height,
496 padding,
497 flex_grow,
498 flex_shrink,
499 aspect_ratio,
500 } => {
501 0.hash(state);
502 hash_opt_unit(*width, state);
503 hash_opt_unit(*height, state);
504 hash_opt_unit(*min_width, state);
505 hash_opt_unit(*max_width, state);
506 hash_opt_unit(*min_height, state);
507 hash_opt_unit(*max_height, state);
508 hash_units(*padding, state);
509 hash_unit(*flex_grow, state);
510 hash_unit(*flex_shrink, state);
511 aspect_ratio.map(|f| f.to_bits()).hash(state);
512 }
513 Self::Flex {
514 direction,
515 wrap,
516 flex_grow,
517 flex_shrink,
518 padding,
519 gap,
520 align_items,
521 justify_content,
522 } => {
523 1.hash(state);
524 direction.hash(state);
525 wrap.hash(state);
526 hash_unit(*flex_grow, state);
527 hash_unit(*flex_shrink, state);
528 hash_units(*padding, state);
529 hash_opt_unit(*gap, state);
530 align_items.hash(state);
531 justify_content.hash(state);
532 }
533 Self::Grid {
534 columns,
535 rows,
536 column_gap,
537 row_gap,
538 padding,
539 } => {
540 2.hash(state);
541 columns.hash(state);
542 rows.hash(state);
543 hash_opt_unit(*column_gap, state);
544 hash_opt_unit(*row_gap, state);
545 hash_units(*padding, state);
546 }
547 Self::GridItem {
548 row_start,
549 row_end,
550 col_start,
551 col_end,
552 } => {
553 3.hash(state);
554 row_start.hash(state);
555 row_end.hash(state);
556 col_start.hash(state);
557 col_end.hash(state);
558 }
559 Self::Scroll {
560 direction,
561 show_scrollbar,
562 width,
563 height,
564 min_width,
565 max_width,
566 min_height,
567 max_height,
568 padding,
569 flex_grow,
570 flex_shrink,
571 } => {
572 4.hash(state);
573 direction.hash(state);
574 show_scrollbar.hash(state);
575 hash_opt_unit(*width, state);
576 hash_opt_unit(*height, state);
577 hash_opt_unit(*min_width, state);
578 hash_opt_unit(*max_width, state);
579 hash_opt_unit(*min_height, state);
580 hash_opt_unit(*max_height, state);
581 hash_units(*padding, state);
582 hash_unit(*flex_grow, state);
583 hash_unit(*flex_shrink, state);
584 }
585 Self::Embed {
586 kind,
587 widget_id,
588 width,
589 height,
590 } => {
591 5.hash(state);
592 kind.hash(state);
593 widget_id.hash(state);
594 hash_opt_unit(*width, state);
595 hash_opt_unit(*height, state);
596 }
597 Self::AbsoluteFill => {
598 6.hash(state);
599 }
600 Self::Positioned {
601 left,
602 top,
603 right,
604 bottom,
605 width,
606 height,
607 } => {
608 7.hash(state);
609 hash_opt_unit(*left, state);
610 hash_opt_unit(*top, state);
611 hash_opt_unit(*right, state);
612 hash_opt_unit(*bottom, state);
613 hash_opt_unit(*width, state);
614 hash_opt_unit(*height, state);
615 }
616 Self::ZStack => {
617 8.hash(state);
618 }
619 Self::Align => {
620 9.hash(state);
621 }
622 Self::Flyout { anchor, content } => {
623 10.hash(state);
624 anchor.hash(state);
625 content.hash(state);
626 }
627 Self::Transform { transform } => {
628 11.hash(state);
629 for v in transform {
630 v.to_bits().hash(state);
631 }
632 }
633 Self::Clip { path } => {
634 12.hash(state);
635 path.hash(state);
636 }
637 }
638 }
639}
640
641#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
642pub struct Color {
643 pub r: u8,
644 pub g: u8,
645 pub b: u8,
646 pub a: u8,
647}
648
649impl Color {
650 pub const BLACK: Self = Self {
651 r: 0,
652 g: 0,
653 b: 0,
654 a: 255,
655 };
656 pub const WHITE: Self = Self {
657 r: 255,
658 g: 255,
659 b: 255,
660 a: 255,
661 };
662 pub const RED: Self = Self {
663 r: 255,
664 g: 0,
665 b: 0,
666 a: 255,
667 };
668 pub const GREEN: Self = Self {
669 r: 0,
670 g: 255,
671 b: 0,
672 a: 255,
673 };
674 pub const BLUE: Self = Self {
675 r: 0,
676 g: 0,
677 b: 255,
678 a: 255,
679 };
680
681 pub fn with_alpha(mut self, a: u8) -> Self {
682 self.a = a;
683 self
684 }
685}
686
687#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
688pub enum Fill {
689 Solid(Color),
690 LinearGradient {
691 start: (f32, f32),
692 end: (f32, f32),
693 stops: Vec<(f32, Color)>,
694 },
695 RadialGradient {
696 center: (f32, f32),
697 radius: f32,
698 stops: Vec<(f32, Color)>,
699 },
700}
701
702impl std::hash::Hash for Fill {
703 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
704 match self {
705 Self::Solid(c) => {
706 0.hash(state);
707 c.hash(state);
708 }
709 Self::LinearGradient { start, end, stops } => {
710 1.hash(state);
711 start.0.to_bits().hash(state);
712 start.1.to_bits().hash(state);
713 end.0.to_bits().hash(state);
714 end.1.to_bits().hash(state);
715 for (off, c) in stops {
716 off.to_bits().hash(state);
717 c.hash(state);
718 }
719 }
720 Self::RadialGradient {
721 center,
722 radius,
723 stops,
724 } => {
725 2.hash(state);
726 center.0.to_bits().hash(state);
727 center.1.to_bits().hash(state);
728 radius.to_bits().hash(state);
729 for (off, c) in stops {
730 off.to_bits().hash(state);
731 c.hash(state);
732 }
733 }
734 }
735 }
736}
737
738#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
739pub enum LineCap {
740 Butt,
741 Round,
742 Square,
743}
744
745#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
746pub enum LineJoin {
747 Miter,
748 Round,
749 Bevel,
750}
751
752#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
753pub struct Stroke {
754 pub fill: Fill,
755 pub width: LayoutUnit,
756 pub dash_array: Option<Vec<f32>>,
757 pub line_cap: LineCap,
758 pub line_join: LineJoin,
759}
760
761impl std::hash::Hash for Stroke {
762 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
763 self.fill.hash(state);
764 self.width.to_bits().hash(state);
765 if let Some(da) = &self.dash_array {
766 1.hash(state);
767 for d in da {
768 d.to_bits().hash(state);
769 }
770 } else {
771 0.hash(state);
772 }
773 self.line_cap.hash(state);
774 self.line_join.hash(state);
775 }
776}
777
778#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
779pub struct BoxShadow {
780 pub color: Color,
781 pub blur_radius: LayoutUnit,
782 pub offset: (LayoutUnit, LayoutUnit),
783}
784
785impl std::hash::Hash for BoxShadow {
786 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
787 self.color.hash(state);
788 self.blur_radius.to_bits().hash(state);
789 self.offset.0.to_bits().hash(state);
790 self.offset.1.to_bits().hash(state);
791 }
792}
793
794#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
795pub enum ImageFit {
796 Contain,
797 Cover,
798 Fill,
799 None,
800}
801
802#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
803pub enum ImageAlignment {
804 TopStart,
805 TopCenter,
806 TopEnd,
807 CenterStart,
808 #[default]
809 Center,
810 CenterEnd,
811 BottomStart,
812 BottomCenter,
813 BottomEnd,
814}
815
816#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
817pub struct HttpHeader {
818 pub name: String,
819 pub value: String,
820}
821
822#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
823pub enum ImageCachePolicy {
824 #[default]
825 Default,
826 Reload,
827 MemoryOnly,
828 Disk,
829 NoStore,
830}
831
832#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
833pub enum ImageSource {
834 Asset {
835 path: String,
836 },
837 File {
838 path: String,
839 },
840 Network {
841 url: String,
842 #[serde(default)]
843 headers: Vec<HttpHeader>,
844 #[serde(default)]
845 cache_policy: ImageCachePolicy,
846 },
847 Memory {
848 bytes: Vec<u8>,
849 #[serde(default)]
850 mime_type: Option<String>,
851 },
852 SvgText {
853 content: String,
854 },
855}
856
857impl Default for ImageSource {
858 fn default() -> Self {
859 Self::Asset {
860 path: String::new(),
861 }
862 }
863}
864
865#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
866pub enum ImageLoadingBehavior {
867 #[default]
868 Empty,
869 ThemePlaceholder,
870 BlurHash(String),
871}
872
873#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
874pub enum ImageErrorBehavior {
875 #[default]
876 Empty,
877 ThemeError,
878 AltText,
879}
880
881#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
882pub struct ImageRequest {
883 pub source: ImageSource,
884 #[serde(default)]
885 pub cache_width: Option<u32>,
886 #[serde(default)]
887 pub cache_height: Option<u32>,
888 #[serde(default)]
889 pub semantic_label: Option<String>,
890 #[serde(default)]
891 pub loading: ImageLoadingBehavior,
892 #[serde(default)]
893 pub error: ImageErrorBehavior,
894}
895
896impl ImageSource {
897 pub fn stable_identity(&self) -> String {
898 match self {
899 Self::Asset { path } => format!("asset:{path}"),
900 Self::File { path } => format!("file:{path}"),
901 Self::Network {
902 url,
903 headers,
904 cache_policy,
905 } => {
906 let mut identity = format!("network:{cache_policy:?}:{url}");
907 for header in headers {
908 identity.push('|');
909 identity.push_str(&header.name.to_ascii_lowercase());
910 identity.push('=');
911 identity.push_str(&header.value);
912 }
913 identity
914 }
915 Self::Memory { bytes, mime_type } => {
916 let digest = blake3::hash(bytes);
917 format!("memory:{}:{digest}", mime_type.as_deref().unwrap_or(""))
918 }
919 Self::SvgText { content } => {
920 let digest = blake3::hash(content.as_bytes());
921 format!("svg:{digest}")
922 }
923 }
924 }
925
926 pub fn local_path(&self) -> Option<&str> {
927 match self {
928 Self::Asset { path } | Self::File { path } => Some(path),
929 _ => None,
930 }
931 }
932
933 pub fn network_url(&self) -> Option<&str> {
934 match self {
935 Self::Network { url, .. } => Some(url),
936 _ => None,
937 }
938 }
939}
940
941impl ImageRequest {
942 pub fn stable_cache_key(&self) -> String {
943 let mut hasher = blake3::Hasher::new();
944 hasher.update(self.source.stable_identity().as_bytes());
945 hasher.update(&self.cache_width.unwrap_or_default().to_le_bytes());
946 hasher.update(&self.cache_height.unwrap_or_default().to_le_bytes());
947 hasher.finalize().to_hex().to_string()
948 }
949}
950
951#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
952pub struct TextStyle {
953 pub font_size: LayoutUnit,
954 pub color: Color,
955 pub underline: bool,
956 #[serde(default)]
957 pub font_family: Option<String>,
958 #[serde(default)]
959 pub locale: Option<String>,
960 #[serde(default = "text_weight_default")]
961 pub font_weight: u16,
962 #[serde(default)]
963 pub font_style: FontStyle,
964 #[serde(default)]
965 pub line_height: Option<LayoutUnit>,
966 #[serde(default)]
967 pub letter_spacing: LayoutUnit,
968 pub background_color: Option<Color>,
970}
971
972impl std::hash::Hash for TextStyle {
973 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
974 self.font_size.to_bits().hash(state);
975 self.color.hash(state);
976 self.underline.hash(state);
977 self.font_family.hash(state);
978 self.locale.hash(state);
979 self.font_weight.hash(state);
980 self.font_style.hash(state);
981 self.line_height.map(f32::to_bits).hash(state);
982 self.letter_spacing.to_bits().hash(state);
983 self.background_color.hash(state);
984 }
985}
986
987#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
988pub enum FontStyle {
989 #[default]
990 Normal,
991 Italic,
992}
993
994const fn text_weight_default() -> u16 {
995 400
996}
997
998#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
999pub struct TextRun {
1000 pub text: String,
1001 pub style: TextStyle,
1002}
1003
1004#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
1005pub struct RichTextAnnotation {
1006 pub range: std::ops::Range<usize>,
1007 #[serde(default)]
1008 pub semantics_label: Option<String>,
1009 #[serde(default)]
1010 pub semantics_identifier: Option<String>,
1011 #[serde(default)]
1012 pub spell_out: Option<bool>,
1013 #[serde(default)]
1014 pub mouse_cursor: Option<MouseCursor>,
1015 #[serde(default)]
1016 pub actions: Vec<ActionEntry>,
1017}
1018
1019pub const INLINE_WIDGET_MARKER_PREFIX: &str = "__fission_inline_widget__:";
1020
1021#[derive(Debug, Clone, Copy, PartialEq)]
1022pub struct InlineWidgetMarker {
1023 pub id: u64,
1024 pub width: LayoutUnit,
1025 pub height: LayoutUnit,
1026}
1027
1028pub fn encode_inline_widget_marker(id: u64, width: LayoutUnit, height: LayoutUnit) -> String {
1029 format!("{INLINE_WIDGET_MARKER_PREFIX}{id}:{width}:{height}")
1030}
1031
1032pub fn decode_inline_widget_marker(family: Option<&str>) -> Option<InlineWidgetMarker> {
1033 let family = family?;
1034 let encoded = family.strip_prefix(INLINE_WIDGET_MARKER_PREFIX)?;
1035 let mut parts = encoded.split(':');
1036 let id = parts.next()?.parse().ok()?;
1037 let width = parts.next()?.parse().ok()?;
1038 let height = parts.next()?.parse().ok()?;
1039 if parts.next().is_some() {
1040 return None;
1041 }
1042 Some(InlineWidgetMarker { id, width, height })
1043}
1044
1045const fn text_wrap_default() -> bool {
1046 true
1047}
1048
1049#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1050pub enum PaintOp {
1051 DrawRect {
1052 fill: Option<Fill>,
1053 stroke: Option<Stroke>,
1054 corner_radius: LayoutUnit,
1055 shadow: Option<BoxShadow>,
1056 },
1057 DrawText {
1058 text: String,
1059 size: LayoutUnit,
1060 color: Color,
1061 underline: bool,
1062 #[serde(default = "text_wrap_default")]
1063 wrap: bool,
1064 caret_index: Option<usize>,
1065 #[serde(default)]
1066 caret_color: Option<Color>,
1067 #[serde(default)]
1068 caret_width: Option<LayoutUnit>,
1069 #[serde(default)]
1070 caret_height: Option<LayoutUnit>,
1071 #[serde(default)]
1072 caret_radius: Option<LayoutUnit>,
1073 #[serde(default)]
1074 paragraph_style: Option<TextParagraphStyle>,
1075 },
1076 DrawRichText {
1077 runs: Vec<TextRun>,
1078 #[serde(default = "text_wrap_default")]
1079 wrap: bool,
1080 caret_index: Option<usize>,
1081 #[serde(default)]
1082 caret_color: Option<Color>,
1083 #[serde(default)]
1084 caret_width: Option<LayoutUnit>,
1085 #[serde(default)]
1086 caret_height: Option<LayoutUnit>,
1087 #[serde(default)]
1088 caret_radius: Option<LayoutUnit>,
1089 #[serde(default)]
1090 paragraph_style: Option<TextParagraphStyle>,
1091 },
1092 DrawImage {
1093 request: ImageRequest,
1094 fit: ImageFit,
1095 alignment: ImageAlignment,
1096 },
1097 DrawPath {
1098 path: String,
1099 fill: Option<Fill>,
1100 stroke: Option<Stroke>,
1101 },
1102 DrawSvg {
1103 content: String,
1104 fill: Option<Fill>,
1105 stroke: Option<Stroke>,
1106 },
1107}
1108
1109impl std::hash::Hash for PaintOp {
1110 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1111 match self {
1112 Self::DrawRect {
1113 fill,
1114 stroke,
1115 corner_radius,
1116 shadow,
1117 } => {
1118 0.hash(state);
1119 fill.hash(state);
1120 stroke.hash(state);
1121 corner_radius.to_bits().hash(state);
1122 shadow.hash(state);
1123 }
1124 Self::DrawText {
1125 text,
1126 size,
1127 color,
1128 underline,
1129 wrap,
1130 caret_index,
1131 caret_color,
1132 caret_width,
1133 caret_height,
1134 caret_radius,
1135 paragraph_style,
1136 } => {
1137 1.hash(state);
1138 text.hash(state);
1139 size.to_bits().hash(state);
1140 color.hash(state);
1141 underline.hash(state);
1142 wrap.hash(state);
1143 caret_index.hash(state);
1144 caret_color.hash(state);
1145 caret_width.map(|w| w.to_bits()).hash(state);
1146 caret_height.map(|h| h.to_bits()).hash(state);
1147 caret_radius.map(|r| r.to_bits()).hash(state);
1148 paragraph_style.hash(state);
1149 }
1150 Self::DrawRichText {
1151 runs,
1152 wrap,
1153 caret_index,
1154 caret_color,
1155 caret_width,
1156 caret_height,
1157 caret_radius,
1158 paragraph_style,
1159 } => {
1160 2.hash(state);
1161 runs.hash(state);
1162 wrap.hash(state);
1163 caret_index.hash(state);
1164 caret_color.hash(state);
1165 caret_width.map(|w| w.to_bits()).hash(state);
1166 caret_height.map(|h| h.to_bits()).hash(state);
1167 caret_radius.map(|r| r.to_bits()).hash(state);
1168 paragraph_style.hash(state);
1169 }
1170 Self::DrawImage {
1171 request,
1172 fit,
1173 alignment,
1174 } => {
1175 3.hash(state);
1176 request.hash(state);
1177 fit.hash(state);
1178 alignment.hash(state);
1179 }
1180 Self::DrawPath { path, fill, stroke } => {
1181 4.hash(state);
1182 path.hash(state);
1183 fill.hash(state);
1184 stroke.hash(state);
1185 }
1186 Self::DrawSvg {
1187 content,
1188 fill,
1189 stroke,
1190 } => {
1191 5.hash(state);
1192 content.hash(state);
1193 fill.hash(state);
1194 stroke.hash(state);
1195 }
1196 }
1197 }
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202 use super::{
1203 decode_inline_widget_marker, decode_text_paragraph_style, encode_inline_widget_marker,
1204 encode_text_paragraph_style, HttpHeader, ImageCachePolicy, ImageRequest, ImageSource,
1205 InlineWidgetMarker, TextAlign, TextDirection, TextHeightBehavior, TextOverflow,
1206 TextParagraphStyle, TextWidthBasis, TEXT_PARAGRAPH_MAX_ENCODED_LINES,
1207 };
1208
1209 #[test]
1210 fn paragraph_style_round_trips_alignment_overflow_and_line_cap() {
1211 let style = TextParagraphStyle {
1212 text_align: TextAlign::Justify,
1213 max_lines: Some(3),
1214 overflow: TextOverflow::Fade,
1215 text_direction: TextDirection::Auto,
1216 text_width_basis: TextWidthBasis::Parent,
1217 strut_line_height: None,
1218 text_height_behavior: TextHeightBehavior::default(),
1219 };
1220
1221 let encoded = encode_text_paragraph_style(style);
1222 assert_eq!(decode_text_paragraph_style(encoded), Some(style));
1223 }
1224
1225 #[test]
1226 fn paragraph_style_clamps_line_count_to_precise_encoding_budget() {
1227 let encoded = encode_text_paragraph_style(TextParagraphStyle {
1228 text_align: TextAlign::End,
1229 max_lines: Some(TEXT_PARAGRAPH_MAX_ENCODED_LINES + 99),
1230 overflow: TextOverflow::Ellipsis,
1231 text_direction: TextDirection::Auto,
1232 text_width_basis: TextWidthBasis::Parent,
1233 strut_line_height: None,
1234 text_height_behavior: TextHeightBehavior::default(),
1235 });
1236
1237 assert_eq!(
1238 decode_text_paragraph_style(encoded),
1239 Some(TextParagraphStyle {
1240 text_align: TextAlign::End,
1241 max_lines: Some(TEXT_PARAGRAPH_MAX_ENCODED_LINES),
1242 overflow: TextOverflow::Ellipsis,
1243 text_direction: TextDirection::Auto,
1244 text_width_basis: TextWidthBasis::Parent,
1245 strut_line_height: None,
1246 text_height_behavior: TextHeightBehavior::default(),
1247 })
1248 );
1249 }
1250
1251 #[test]
1252 fn image_request_cache_key_is_stable_and_dimension_sensitive() {
1253 let request = ImageRequest {
1254 source: ImageSource::Network {
1255 url: "https://cdn.example.com/image.webp".into(),
1256 headers: vec![HttpHeader {
1257 name: "Accept".into(),
1258 value: "image/webp".into(),
1259 }],
1260 cache_policy: ImageCachePolicy::Default,
1261 },
1262 cache_width: Some(320),
1263 cache_height: Some(180),
1264 ..Default::default()
1265 };
1266
1267 let same = request.clone();
1268 let mut resized = request.clone();
1269 resized.cache_width = Some(640);
1270
1271 assert_eq!(request.stable_cache_key(), same.stable_cache_key());
1272 assert_ne!(request.stable_cache_key(), resized.stable_cache_key());
1273 }
1274
1275 #[test]
1276 fn image_source_helpers_report_path_and_network_sources() {
1277 assert_eq!(
1278 ImageSource::Asset {
1279 path: "assets/logo.png".into()
1280 }
1281 .local_path(),
1282 Some("assets/logo.png")
1283 );
1284 assert_eq!(
1285 ImageSource::Network {
1286 url: "https://example.com/logo.png".into(),
1287 headers: Vec::new(),
1288 cache_policy: ImageCachePolicy::Default,
1289 }
1290 .network_url(),
1291 Some("https://example.com/logo.png")
1292 );
1293 }
1294
1295 #[test]
1296 fn paragraph_style_compact_encoding_rejects_extended_fields() {
1297 assert_eq!(
1298 encode_text_paragraph_style(TextParagraphStyle {
1299 text_align: TextAlign::Start,
1300 max_lines: Some(2),
1301 overflow: TextOverflow::Visible,
1302 text_direction: TextDirection::Rtl,
1303 text_width_basis: TextWidthBasis::LongestLine,
1304 strut_line_height: Some(24.0),
1305 text_height_behavior: TextHeightBehavior {
1306 apply_height_to_first_ascent: false,
1307 apply_height_to_last_descent: true,
1308 },
1309 }),
1310 None
1311 );
1312 }
1313
1314 #[test]
1315 fn inline_widget_marker_round_trips() {
1316 let encoded = encode_inline_widget_marker(7, 24.5, 12.0);
1317 assert_eq!(
1318 decode_inline_widget_marker(Some(encoded.as_str())),
1319 Some(InlineWidgetMarker {
1320 id: 7,
1321 width: 24.5,
1322 height: 12.0,
1323 })
1324 );
1325 }
1326}