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