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 motion_target: Option<WidgetId>,
47}
48
49impl CompositeScalar {
50 pub fn new(base: f32) -> Self {
51 Self {
52 base,
53 motion_target: None,
54 }
55 }
56
57 pub fn motion(mut self, target: WidgetId) -> Self {
58 self.motion_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.motion_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, PartialEq, Serialize, Deserialize)]
85pub enum Length {
86 Points(LayoutUnit),
88 Percent(f32),
90 ViewportWidth(f32),
92 ViewportHeight(f32),
94 Add(Box<Length>, Box<Length>),
96 Subtract(Box<Length>, Box<Length>),
98 Min(Vec<Length>),
100 Max(Vec<Length>),
102 Clamp {
104 min: Box<Length>,
106 preferred: Box<Length>,
108 max: Box<Length>,
110 },
111 FitContent(Option<Box<Length>>),
113 MinContent,
115 MaxContent,
117 Auto,
119}
120
121impl Length {
122 pub fn points(value: LayoutUnit) -> Self {
124 Self::Points(value)
125 }
126
127 pub fn percent(value: f32) -> Self {
129 Self::Percent(value)
130 }
131
132 pub fn vw(value: f32) -> Self {
134 Self::ViewportWidth(value)
135 }
136
137 pub fn vh(value: f32) -> Self {
139 Self::ViewportHeight(value)
140 }
141
142 pub fn clamp(min: Length, preferred: Length, max: Length) -> Self {
144 Self::Clamp {
145 min: Box::new(min),
146 preferred: Box::new(preferred),
147 max: Box::new(max),
148 }
149 }
150
151 pub fn min(values: impl Into<Vec<Length>>) -> Self {
153 Self::Min(values.into())
154 }
155
156 pub fn max(values: impl Into<Vec<Length>>) -> Self {
158 Self::Max(values.into())
159 }
160
161 pub fn fit_content(limit: impl Into<Option<Length>>) -> Self {
163 Self::FitContent(limit.into().map(Box::new))
164 }
165
166 pub fn all(value: Length) -> [Length; 4] {
168 std::array::from_fn(|_| value.clone())
169 }
170
171 pub fn symmetric(horizontal: Length, vertical: Length) -> [Length; 4] {
173 [horizontal.clone(), horizontal, vertical.clone(), vertical]
174 }
175
176 pub fn resolve(
181 &self,
182 reference: LayoutUnit,
183 viewport_width: LayoutUnit,
184 viewport_height: LayoutUnit,
185 ) -> Option<LayoutUnit> {
186 let resolved = match self {
187 Self::Points(value) => *value,
188 Self::Percent(value) => reference.is_finite().then_some(reference * value / 100.0)?,
189 Self::ViewportWidth(value) => viewport_width * value / 100.0,
190 Self::ViewportHeight(value) => viewport_height * value / 100.0,
191 Self::Add(left, right) => {
192 left.resolve(reference, viewport_width, viewport_height)?
193 + right.resolve(reference, viewport_width, viewport_height)?
194 }
195 Self::Subtract(left, right) => {
196 left.resolve(reference, viewport_width, viewport_height)?
197 - right.resolve(reference, viewport_width, viewport_height)?
198 }
199 Self::Min(values) => resolve_length_list(
200 values,
201 reference,
202 viewport_width,
203 viewport_height,
204 LayoutUnit::min,
205 )?,
206 Self::Max(values) => resolve_length_list(
207 values,
208 reference,
209 viewport_width,
210 viewport_height,
211 LayoutUnit::max,
212 )?,
213 Self::Clamp {
214 min,
215 preferred,
216 max,
217 } => {
218 let minimum = min.resolve(reference, viewport_width, viewport_height)?;
219 let maximum = max.resolve(reference, viewport_width, viewport_height)?;
220 preferred
221 .resolve(reference, viewport_width, viewport_height)?
222 .clamp(minimum.min(maximum), minimum.max(maximum))
223 }
224 Self::FitContent(_) | Self::MinContent | Self::MaxContent | Self::Auto => return None,
225 };
226 resolved.is_finite().then_some(resolved)
227 }
228}
229
230fn resolve_length_list(
231 values: &[Length],
232 reference: LayoutUnit,
233 viewport_width: LayoutUnit,
234 viewport_height: LayoutUnit,
235 combine: impl Fn(LayoutUnit, LayoutUnit) -> LayoutUnit,
236) -> Option<LayoutUnit> {
237 let mut values = values.iter();
238 let mut resolved = values
239 .next()?
240 .resolve(reference, viewport_width, viewport_height)?;
241 for value in values {
242 resolved = combine(
243 resolved,
244 value.resolve(reference, viewport_width, viewport_height)?,
245 );
246 }
247 Some(resolved)
248}
249
250impl From<LayoutUnit> for Length {
251 fn from(value: LayoutUnit) -> Self {
252 Self::Points(value)
253 }
254}
255
256impl std::ops::Add for Length {
257 type Output = Self;
258
259 fn add(self, rhs: Self) -> Self::Output {
260 Self::Add(Box::new(self), Box::new(rhs))
261 }
262}
263
264impl std::ops::Sub for Length {
265 type Output = Self;
266
267 fn sub(self, rhs: Self) -> Self::Output {
268 Self::Subtract(Box::new(self), Box::new(rhs))
269 }
270}
271
272impl std::hash::Hash for Length {
273 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
274 std::mem::discriminant(self).hash(state);
275 match self {
276 Self::Points(value)
277 | Self::Percent(value)
278 | Self::ViewportWidth(value)
279 | Self::ViewportHeight(value) => value.to_bits().hash(state),
280 Self::Add(left, right) | Self::Subtract(left, right) => {
281 left.hash(state);
282 right.hash(state);
283 }
284 Self::Min(values) | Self::Max(values) => values.hash(state),
285 Self::Clamp {
286 min,
287 preferred,
288 max,
289 } => {
290 min.hash(state);
291 preferred.hash(state);
292 max.hash(state);
293 }
294 Self::FitContent(limit) => limit.hash(state),
295 Self::MinContent | Self::MaxContent | Self::Auto => {}
296 }
297 }
298}
299
300#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
302pub enum Overflow {
303 #[default]
305 Visible,
306 Clip,
308}
309
310#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
312pub enum BoxAlignment {
313 #[default]
315 Start,
316 Center,
318 End,
320 Stretch,
322}
323
324#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, Hash)]
326pub struct BoxPosition {
327 pub left: Option<Length>,
329 pub top: Option<Length>,
331 pub right: Option<Length>,
333 pub bottom: Option<Length>,
335}
336
337#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
339pub struct BoxGridPlacement {
340 pub row_start: GridPlacement,
342 pub row_end: GridPlacement,
344 pub col_start: GridPlacement,
346 pub col_end: GridPlacement,
348}
349
350#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, Hash)]
371pub struct BoxStyle {
372 pub width: Option<Length>,
374 pub height: Option<Length>,
376 pub min_width: Option<Length>,
378 pub max_width: Option<Length>,
380 pub min_height: Option<Length>,
382 pub max_height: Option<Length>,
384 pub padding: Option<[Length; 4]>,
386 pub margin: Option<[Length; 4]>,
388 pub aspect_ratio: Option<OrderedLayoutUnit>,
390 pub overflow: Overflow,
392 pub alignment: BoxAlignment,
394 pub position: Option<BoxPosition>,
396 pub grid: Option<BoxGridPlacement>,
398 pub flex_grow: Option<OrderedLayoutUnit>,
400 pub flex_shrink: Option<OrderedLayoutUnit>,
402}
403
404impl BoxStyle {
405 pub fn width(mut self, value: Length) -> Self {
407 self.width = Some(value);
408 self
409 }
410
411 pub fn height(mut self, value: Length) -> Self {
413 self.height = Some(value);
414 self
415 }
416
417 pub fn min_width(mut self, value: Length) -> Self {
419 self.min_width = Some(value);
420 self
421 }
422
423 pub fn max_width(mut self, value: Length) -> Self {
425 self.max_width = Some(value);
426 self
427 }
428
429 pub fn min_height(mut self, value: Length) -> Self {
431 self.min_height = Some(value);
432 self
433 }
434
435 pub fn max_height(mut self, value: Length) -> Self {
437 self.max_height = Some(value);
438 self
439 }
440
441 pub fn padding(mut self, edges: [Length; 4]) -> Self {
443 self.padding = Some(edges);
444 self
445 }
446
447 pub fn padding_all(self, value: Length) -> Self {
449 self.padding(Length::all(value))
450 }
451
452 pub fn padding_symmetric(self, horizontal: Length, vertical: Length) -> Self {
454 self.padding(Length::symmetric(horizontal, vertical))
455 }
456
457 pub fn margin(mut self, edges: [Length; 4]) -> Self {
459 self.margin = Some(edges);
460 self
461 }
462
463 pub fn margin_all(self, value: Length) -> Self {
465 self.margin(Length::all(value))
466 }
467
468 pub fn margin_symmetric(self, horizontal: Length, vertical: Length) -> Self {
470 self.margin(Length::symmetric(horizontal, vertical))
471 }
472
473 pub fn overflow(mut self, overflow: Overflow) -> Self {
475 self.overflow = overflow;
476 self
477 }
478
479 pub fn align(mut self, alignment: BoxAlignment) -> Self {
481 self.alignment = alignment;
482 self
483 }
484
485 pub fn aspect_ratio(mut self, ratio: LayoutUnit) -> Self {
487 self.aspect_ratio = Some(OrderedLayoutUnit(ratio.max(0.0)));
488 self
489 }
490
491 pub fn positioned(mut self, position: BoxPosition) -> Self {
493 self.position = Some(position);
494 self
495 }
496
497 pub fn grid(mut self, placement: BoxGridPlacement) -> Self {
499 self.grid = Some(placement);
500 self
501 }
502
503 pub fn flex(mut self, grow: LayoutUnit, shrink: LayoutUnit) -> Self {
505 self.flex_grow = Some(OrderedLayoutUnit(grow));
506 self.flex_shrink = Some(OrderedLayoutUnit(shrink));
507 self
508 }
509}
510
511#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
513pub struct OrderedLayoutUnit(
514 pub LayoutUnit,
516);
517
518impl std::hash::Hash for OrderedLayoutUnit {
519 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
520 self.0.to_bits().hash(state);
521 }
522}
523
524#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
525pub enum TextAlign {
526 Left,
527 Right,
528 Center,
529 Justify,
530 #[default]
531 Start,
532 End,
533}
534
535#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
536pub enum TextOverflow {
537 Clip,
538 Ellipsis,
539 Fade,
540 #[default]
541 Visible,
542}
543
544#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
545pub enum TextDirection {
546 #[default]
547 Auto,
548 Ltr,
549 Rtl,
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
553pub enum TextWidthBasis {
554 #[default]
555 Parent,
556 LongestLine,
557}
558
559#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
560pub enum MouseCursor {
561 #[default]
562 Basic,
563 Pointer,
564 Text,
565}
566
567#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
568pub struct TextHeightBehavior {
569 pub apply_height_to_first_ascent: bool,
570 pub apply_height_to_last_descent: bool,
571}
572
573impl Default for TextHeightBehavior {
574 fn default() -> Self {
575 Self {
576 apply_height_to_first_ascent: true,
577 apply_height_to_last_descent: true,
578 }
579 }
580}
581
582#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
583pub struct TextParagraphStyle {
584 pub text_align: TextAlign,
585 pub max_lines: Option<usize>,
586 pub overflow: TextOverflow,
587 #[serde(default)]
588 pub text_direction: TextDirection,
589 #[serde(default)]
590 pub text_width_basis: TextWidthBasis,
591 #[serde(default)]
592 pub strut_line_height: Option<LayoutUnit>,
593 #[serde(default)]
594 pub text_height_behavior: TextHeightBehavior,
595}
596
597impl PartialEq for TextParagraphStyle {
598 fn eq(&self, other: &Self) -> bool {
599 self.text_align == other.text_align
600 && self.max_lines == other.max_lines
601 && self.overflow == other.overflow
602 && self.text_direction == other.text_direction
603 && self.text_width_basis == other.text_width_basis
604 && self.strut_line_height.map(f32::to_bits) == other.strut_line_height.map(f32::to_bits)
605 && self.text_height_behavior == other.text_height_behavior
606 }
607}
608
609impl Eq for TextParagraphStyle {}
610
611impl std::hash::Hash for TextParagraphStyle {
612 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
613 self.text_align.hash(state);
614 self.max_lines.hash(state);
615 self.overflow.hash(state);
616 self.text_direction.hash(state);
617 self.text_width_basis.hash(state);
618 self.strut_line_height.map(f32::to_bits).hash(state);
619 self.text_height_behavior.hash(state);
620 }
621}
622
623const TEXT_PARAGRAPH_ALIGN_BITS: u32 = 0b111;
624const TEXT_PARAGRAPH_OVERFLOW_BITS: u32 = 0b111 << 3;
625const TEXT_PARAGRAPH_MAX_LINES_SHIFT: u32 = 6;
626const TEXT_PARAGRAPH_SENTINEL: u32 = 1;
627const TEXT_PARAGRAPH_MAX_ENCODED_LINES: usize = ((1 << 24) - 1) >> TEXT_PARAGRAPH_MAX_LINES_SHIFT;
628
629const fn text_align_code(align: TextAlign) -> u32 {
630 match align {
631 TextAlign::Start => 0,
632 TextAlign::Left => 1,
633 TextAlign::Center => 2,
634 TextAlign::Right => 3,
635 TextAlign::End => 4,
636 TextAlign::Justify => 5,
637 }
638}
639
640const fn text_overflow_code(overflow: TextOverflow) -> u32 {
641 match overflow {
642 TextOverflow::Visible => 0,
643 TextOverflow::Clip => 1,
644 TextOverflow::Ellipsis => 2,
645 TextOverflow::Fade => 3,
646 }
647}
648
649const fn decode_text_align(code: u32) -> TextAlign {
650 match code {
651 1 => TextAlign::Left,
652 2 => TextAlign::Center,
653 3 => TextAlign::Right,
654 4 => TextAlign::End,
655 5 => TextAlign::Justify,
656 _ => TextAlign::Start,
657 }
658}
659
660const fn decode_text_overflow(code: u32) -> TextOverflow {
661 match code {
662 1 => TextOverflow::Clip,
663 2 => TextOverflow::Ellipsis,
664 3 => TextOverflow::Fade,
665 _ => TextOverflow::Visible,
666 }
667}
668
669pub fn encode_text_paragraph_style(style: TextParagraphStyle) -> Option<LayoutUnit> {
670 if style == TextParagraphStyle::default() {
671 return None;
672 }
673 if style.text_direction != TextDirection::Auto
674 || style.text_width_basis != TextWidthBasis::Parent
675 || style.strut_line_height.is_some()
676 || style.text_height_behavior != TextHeightBehavior::default()
677 {
678 return None;
679 }
680
681 let max_lines = style
682 .max_lines
683 .unwrap_or(0)
684 .min(TEXT_PARAGRAPH_MAX_ENCODED_LINES) as u32;
685 let encoded = TEXT_PARAGRAPH_SENTINEL
686 + text_align_code(style.text_align)
687 + (text_overflow_code(style.overflow) << 3)
688 + (max_lines << TEXT_PARAGRAPH_MAX_LINES_SHIFT);
689
690 Some(-(encoded as LayoutUnit))
691}
692
693pub fn decode_text_paragraph_style(
694 encoded_width: Option<LayoutUnit>,
695) -> Option<TextParagraphStyle> {
696 let encoded_width = encoded_width?;
697 if !encoded_width.is_finite() || encoded_width >= 0.0 {
698 return None;
699 }
700
701 let raw = (-encoded_width).round();
702 if raw < TEXT_PARAGRAPH_SENTINEL as f32 {
703 return None;
704 }
705
706 let bits = raw as u32 - TEXT_PARAGRAPH_SENTINEL;
707 let text_align = decode_text_align(bits & TEXT_PARAGRAPH_ALIGN_BITS);
708 let overflow = decode_text_overflow((bits & TEXT_PARAGRAPH_OVERFLOW_BITS) >> 3);
709 let max_lines = match bits >> TEXT_PARAGRAPH_MAX_LINES_SHIFT {
710 0 => None,
711 lines => Some(lines as usize),
712 };
713
714 Some(TextParagraphStyle {
715 text_align,
716 max_lines,
717 overflow,
718 text_direction: TextDirection::Auto,
719 text_width_basis: TextWidthBasis::Parent,
720 strut_line_height: None,
721 text_height_behavior: TextHeightBehavior::default(),
722 })
723}
724
725#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
726pub enum FlexDirection {
727 Row,
728 Column,
729}
730
731impl Default for FlexDirection {
732 fn default() -> Self {
733 FlexDirection::Row
734 }
735}
736
737#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
738pub enum EmbedKind {
739 Video,
740 Web,
741 Custom(Vec<u8>),
742}
743
744#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
745pub enum GridTrack {
746 Points(LayoutUnit),
748 Percent(f32),
750 Fr(f32),
752 Auto,
754 MinContent,
756 MaxContent,
758 MinMax(Box<GridTrack>, Box<GridTrack>),
760 Repeat { count: u16, tracks: Vec<GridTrack> },
762 AutoFit(Box<GridTrack>),
764 AutoFill(Box<GridTrack>),
766}
767
768#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
770pub enum ResponsiveQuery {
771 #[default]
773 Viewport,
774 Container,
776}
777
778#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
780pub struct ResponsiveCondition {
781 pub min_width: Option<LayoutUnit>,
783 pub max_width: Option<LayoutUnit>,
785}
786
787impl ResponsiveCondition {
788 pub fn matches(self, width: LayoutUnit) -> bool {
789 self.min_width.is_none_or(|minimum| width >= minimum)
790 && self.max_width.is_none_or(|maximum| width < maximum)
791 }
792}
793
794impl std::hash::Hash for ResponsiveCondition {
795 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
796 self.min_width.map(f32::to_bits).hash(state);
797 self.max_width.map(f32::to_bits).hash(state);
798 }
799}
800
801impl GridTrack {
802 pub fn minmax(min: GridTrack, max: GridTrack) -> Self {
812 Self::MinMax(Box::new(min), Box::new(max))
813 }
814
815 pub fn repeat(count: u16, tracks: impl Into<Vec<GridTrack>>) -> Self {
817 Self::Repeat {
818 count,
819 tracks: tracks.into(),
820 }
821 }
822
823 pub fn auto_fit(track: GridTrack) -> Self {
825 Self::AutoFit(Box::new(track))
826 }
827
828 pub fn auto_fill(track: GridTrack) -> Self {
830 Self::AutoFill(Box::new(track))
831 }
832}
833
834impl std::hash::Hash for GridTrack {
835 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
836 match self {
837 Self::Points(u) => {
838 0.hash(state);
839 u.to_bits().hash(state);
840 }
841 Self::Percent(f) => {
842 1.hash(state);
843 f.to_bits().hash(state);
844 }
845 Self::Fr(f) => {
846 2.hash(state);
847 f.to_bits().hash(state);
848 }
849 Self::Auto => {
850 3.hash(state);
851 }
852 Self::MinContent => {
853 4.hash(state);
854 }
855 Self::MaxContent => {
856 5.hash(state);
857 }
858 Self::MinMax(min, max) => {
859 6.hash(state);
860 min.hash(state);
861 max.hash(state);
862 }
863 Self::Repeat { count, tracks } => {
864 7.hash(state);
865 count.hash(state);
866 tracks.hash(state);
867 }
868 Self::AutoFit(track) => {
869 8.hash(state);
870 track.hash(state);
871 }
872 Self::AutoFill(track) => {
873 9.hash(state);
874 track.hash(state);
875 }
876 }
877 }
878}
879
880#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
881pub enum GridPlacement {
882 Auto,
884 Line(i16),
886 Span(u16),
888}
889
890impl Default for GridPlacement {
891 fn default() -> Self {
892 Self::Auto
893 }
894}
895
896#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
897pub enum FlexWrap {
898 NoWrap,
899 Wrap,
900 WrapReverse,
901}
902
903impl Default for FlexWrap {
904 fn default() -> Self {
905 FlexWrap::NoWrap
906 }
907}
908
909#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
910pub enum AlignItems {
911 Start,
912 End,
913 Center,
914 Stretch,
915 Baseline,
916}
917
918impl Default for AlignItems {
919 fn default() -> Self {
920 AlignItems::Stretch
921 }
922}
923
924#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
925pub enum JustifyContent {
926 Start,
927 End,
928 Center,
929 SpaceBetween,
930 SpaceAround,
931 SpaceEvenly,
932}
933
934impl Default for JustifyContent {
935 fn default() -> Self {
936 JustifyContent::Start
937 }
938}
939
940#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
941pub enum LayoutOp {
942 Box {
943 width: Option<LayoutUnit>,
944 height: Option<LayoutUnit>,
945 min_width: Option<LayoutUnit>,
946 max_width: Option<LayoutUnit>,
947 min_height: Option<LayoutUnit>,
948 max_height: Option<LayoutUnit>,
949 padding: [LayoutUnit; 4],
950 flex_grow: LayoutUnit,
951 flex_shrink: LayoutUnit,
952 aspect_ratio: Option<f32>,
953 },
954 StyledBox {
956 style: BoxStyle,
957 flex_grow: LayoutUnit,
958 flex_shrink: LayoutUnit,
959 },
960 Flex {
961 direction: FlexDirection,
962 wrap: FlexWrap,
963 flex_grow: LayoutUnit,
964 flex_shrink: LayoutUnit,
965 padding: [LayoutUnit; 4],
966 gap: Option<LayoutUnit>,
967 align_items: AlignItems,
968 justify_content: JustifyContent,
969 },
970 Grid {
971 columns: Vec<GridTrack>,
972 rows: Vec<GridTrack>,
973 column_gap: Option<LayoutUnit>,
974 row_gap: Option<LayoutUnit>,
975 padding: [LayoutUnit; 4],
976 },
977 GridItem {
978 row_start: GridPlacement,
979 row_end: GridPlacement,
980 col_start: GridPlacement,
981 col_end: GridPlacement,
982 },
983 Responsive {
985 query: ResponsiveQuery,
986 cases: Vec<ResponsiveCondition>,
987 },
988 Scroll {
989 direction: FlexDirection,
990 show_scrollbar: bool,
991 width: Option<LayoutUnit>,
992 height: Option<LayoutUnit>,
993 min_width: Option<LayoutUnit>,
994 max_width: Option<LayoutUnit>,
995 min_height: Option<LayoutUnit>,
996 max_height: Option<LayoutUnit>,
997 padding: [LayoutUnit; 4],
998 flex_grow: LayoutUnit,
999 flex_shrink: LayoutUnit,
1000 },
1001 Embed {
1002 kind: EmbedKind,
1003 widget_id: WidgetId,
1004 width: Option<LayoutUnit>,
1005 height: Option<LayoutUnit>,
1006 },
1007 AbsoluteFill,
1008 Positioned {
1009 left: Option<LayoutUnit>,
1010 top: Option<LayoutUnit>,
1011 right: Option<LayoutUnit>,
1012 bottom: Option<LayoutUnit>,
1013 width: Option<LayoutUnit>,
1014 height: Option<LayoutUnit>,
1015 },
1016 PositionedLengths {
1018 left: Option<Length>,
1019 top: Option<Length>,
1020 right: Option<Length>,
1021 bottom: Option<Length>,
1022 width: Option<Length>,
1023 height: Option<Length>,
1024 },
1025 ZStack,
1026 Align,
1027 Flyout {
1028 anchor: WidgetId,
1029 content: WidgetId,
1030 },
1031 Transform {
1032 transform: [f32; 16],
1033 },
1034 Clip {
1035 path: Option<String>,
1036 },
1037}
1038
1039impl std::hash::Hash for LayoutOp {
1040 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1041 let hash_unit = |u: LayoutUnit, h: &mut H| u.to_bits().hash(h);
1042 let hash_opt_unit = |u: Option<LayoutUnit>, h: &mut H| u.map(|v| v.to_bits()).hash(h);
1043 let hash_units = |us: [LayoutUnit; 4], h: &mut H| {
1044 for u in us {
1045 u.to_bits().hash(h);
1046 }
1047 };
1048
1049 match self {
1050 Self::Box {
1051 width,
1052 height,
1053 min_width,
1054 max_width,
1055 min_height,
1056 max_height,
1057 padding,
1058 flex_grow,
1059 flex_shrink,
1060 aspect_ratio,
1061 } => {
1062 0.hash(state);
1063 hash_opt_unit(*width, state);
1064 hash_opt_unit(*height, state);
1065 hash_opt_unit(*min_width, state);
1066 hash_opt_unit(*max_width, state);
1067 hash_opt_unit(*min_height, state);
1068 hash_opt_unit(*max_height, state);
1069 hash_units(*padding, state);
1070 hash_unit(*flex_grow, state);
1071 hash_unit(*flex_shrink, state);
1072 aspect_ratio.map(|f| f.to_bits()).hash(state);
1073 }
1074 Self::StyledBox {
1075 style,
1076 flex_grow,
1077 flex_shrink,
1078 } => {
1079 13.hash(state);
1080 style.hash(state);
1081 hash_unit(*flex_grow, state);
1082 hash_unit(*flex_shrink, state);
1083 }
1084 Self::Flex {
1085 direction,
1086 wrap,
1087 flex_grow,
1088 flex_shrink,
1089 padding,
1090 gap,
1091 align_items,
1092 justify_content,
1093 } => {
1094 1.hash(state);
1095 direction.hash(state);
1096 wrap.hash(state);
1097 hash_unit(*flex_grow, state);
1098 hash_unit(*flex_shrink, state);
1099 hash_units(*padding, state);
1100 hash_opt_unit(*gap, state);
1101 align_items.hash(state);
1102 justify_content.hash(state);
1103 }
1104 Self::Grid {
1105 columns,
1106 rows,
1107 column_gap,
1108 row_gap,
1109 padding,
1110 } => {
1111 2.hash(state);
1112 columns.hash(state);
1113 rows.hash(state);
1114 hash_opt_unit(*column_gap, state);
1115 hash_opt_unit(*row_gap, state);
1116 hash_units(*padding, state);
1117 }
1118 Self::GridItem {
1119 row_start,
1120 row_end,
1121 col_start,
1122 col_end,
1123 } => {
1124 3.hash(state);
1125 row_start.hash(state);
1126 row_end.hash(state);
1127 col_start.hash(state);
1128 col_end.hash(state);
1129 }
1130 Self::Responsive { query, cases } => {
1131 14.hash(state);
1132 query.hash(state);
1133 cases.hash(state);
1134 }
1135 Self::Scroll {
1136 direction,
1137 show_scrollbar,
1138 width,
1139 height,
1140 min_width,
1141 max_width,
1142 min_height,
1143 max_height,
1144 padding,
1145 flex_grow,
1146 flex_shrink,
1147 } => {
1148 4.hash(state);
1149 direction.hash(state);
1150 show_scrollbar.hash(state);
1151 hash_opt_unit(*width, state);
1152 hash_opt_unit(*height, state);
1153 hash_opt_unit(*min_width, state);
1154 hash_opt_unit(*max_width, state);
1155 hash_opt_unit(*min_height, state);
1156 hash_opt_unit(*max_height, state);
1157 hash_units(*padding, state);
1158 hash_unit(*flex_grow, state);
1159 hash_unit(*flex_shrink, state);
1160 }
1161 Self::Embed {
1162 kind,
1163 widget_id,
1164 width,
1165 height,
1166 } => {
1167 5.hash(state);
1168 kind.hash(state);
1169 widget_id.hash(state);
1170 hash_opt_unit(*width, state);
1171 hash_opt_unit(*height, state);
1172 }
1173 Self::AbsoluteFill => {
1174 6.hash(state);
1175 }
1176 Self::Positioned {
1177 left,
1178 top,
1179 right,
1180 bottom,
1181 width,
1182 height,
1183 } => {
1184 7.hash(state);
1185 hash_opt_unit(*left, state);
1186 hash_opt_unit(*top, state);
1187 hash_opt_unit(*right, state);
1188 hash_opt_unit(*bottom, state);
1189 hash_opt_unit(*width, state);
1190 hash_opt_unit(*height, state);
1191 }
1192 Self::PositionedLengths {
1193 left,
1194 top,
1195 right,
1196 bottom,
1197 width,
1198 height,
1199 } => {
1200 15.hash(state);
1201 left.hash(state);
1202 top.hash(state);
1203 right.hash(state);
1204 bottom.hash(state);
1205 width.hash(state);
1206 height.hash(state);
1207 }
1208 Self::ZStack => {
1209 8.hash(state);
1210 }
1211 Self::Align => {
1212 9.hash(state);
1213 }
1214 Self::Flyout { anchor, content } => {
1215 10.hash(state);
1216 anchor.hash(state);
1217 content.hash(state);
1218 }
1219 Self::Transform { transform } => {
1220 11.hash(state);
1221 for v in transform {
1222 v.to_bits().hash(state);
1223 }
1224 }
1225 Self::Clip { path } => {
1226 12.hash(state);
1227 path.hash(state);
1228 }
1229 }
1230 }
1231}
1232
1233#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
1234pub struct Color {
1235 pub r: u8,
1236 pub g: u8,
1237 pub b: u8,
1238 pub a: u8,
1239}
1240
1241impl Color {
1242 pub const TRANSPARENT: Self = Self {
1243 r: 0,
1244 g: 0,
1245 b: 0,
1246 a: 0,
1247 };
1248 pub const BLACK: Self = Self {
1249 r: 0,
1250 g: 0,
1251 b: 0,
1252 a: 255,
1253 };
1254 pub const WHITE: Self = Self {
1255 r: 255,
1256 g: 255,
1257 b: 255,
1258 a: 255,
1259 };
1260 pub const RED: Self = Self {
1261 r: 255,
1262 g: 0,
1263 b: 0,
1264 a: 255,
1265 };
1266 pub const GREEN: Self = Self {
1267 r: 0,
1268 g: 255,
1269 b: 0,
1270 a: 255,
1271 };
1272 pub const BLUE: Self = Self {
1273 r: 0,
1274 g: 0,
1275 b: 255,
1276 a: 255,
1277 };
1278
1279 pub fn with_alpha(mut self, a: u8) -> Self {
1280 self.a = a;
1281 self
1282 }
1283}
1284
1285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1286pub enum Fill {
1287 Solid(Color),
1288 LinearGradient {
1289 start: (f32, f32),
1290 end: (f32, f32),
1291 stops: Vec<(f32, Color)>,
1292 },
1293 RadialGradient {
1294 center: (f32, f32),
1295 radius: f32,
1296 stops: Vec<(f32, Color)>,
1297 },
1298}
1299
1300impl std::hash::Hash for Fill {
1301 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1302 match self {
1303 Self::Solid(c) => {
1304 0.hash(state);
1305 c.hash(state);
1306 }
1307 Self::LinearGradient { start, end, stops } => {
1308 1.hash(state);
1309 start.0.to_bits().hash(state);
1310 start.1.to_bits().hash(state);
1311 end.0.to_bits().hash(state);
1312 end.1.to_bits().hash(state);
1313 for (off, c) in stops {
1314 off.to_bits().hash(state);
1315 c.hash(state);
1316 }
1317 }
1318 Self::RadialGradient {
1319 center,
1320 radius,
1321 stops,
1322 } => {
1323 2.hash(state);
1324 center.0.to_bits().hash(state);
1325 center.1.to_bits().hash(state);
1326 radius.to_bits().hash(state);
1327 for (off, c) in stops {
1328 off.to_bits().hash(state);
1329 c.hash(state);
1330 }
1331 }
1332 }
1333 }
1334}
1335
1336#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1337pub enum LineCap {
1338 Butt,
1339 Round,
1340 Square,
1341}
1342
1343#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1344pub enum LineJoin {
1345 Miter,
1346 Round,
1347 Bevel,
1348}
1349
1350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1351pub struct Stroke {
1352 pub fill: Fill,
1353 pub width: LayoutUnit,
1354 pub dash_array: Option<Vec<f32>>,
1355 pub line_cap: LineCap,
1356 pub line_join: LineJoin,
1357}
1358
1359impl std::hash::Hash for Stroke {
1360 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1361 self.fill.hash(state);
1362 self.width.to_bits().hash(state);
1363 if let Some(da) = &self.dash_array {
1364 1.hash(state);
1365 for d in da {
1366 d.to_bits().hash(state);
1367 }
1368 } else {
1369 0.hash(state);
1370 }
1371 self.line_cap.hash(state);
1372 self.line_join.hash(state);
1373 }
1374}
1375
1376#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1377pub struct BoxShadow {
1378 pub color: Color,
1379 pub blur_radius: LayoutUnit,
1380 pub spread_radius: LayoutUnit,
1382 pub offset: (LayoutUnit, LayoutUnit),
1383 pub inset: bool,
1385}
1386
1387impl std::hash::Hash for BoxShadow {
1388 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1389 self.color.hash(state);
1390 self.blur_radius.to_bits().hash(state);
1391 self.spread_radius.to_bits().hash(state);
1392 self.offset.0.to_bits().hash(state);
1393 self.offset.1.to_bits().hash(state);
1394 self.inset.hash(state);
1395 }
1396}
1397
1398#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
1399pub enum ImageFit {
1400 Contain,
1401 Cover,
1402 Fill,
1403 None,
1404}
1405
1406#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1407pub enum ImageAlignment {
1408 TopStart,
1409 TopCenter,
1410 TopEnd,
1411 CenterStart,
1412 #[default]
1413 Center,
1414 CenterEnd,
1415 BottomStart,
1416 BottomCenter,
1417 BottomEnd,
1418}
1419
1420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
1421pub struct HttpHeader {
1422 pub name: String,
1423 pub value: String,
1424}
1425
1426#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1427pub enum ImageCachePolicy {
1428 #[default]
1429 Default,
1430 Reload,
1431 MemoryOnly,
1432 Disk,
1433 NoStore,
1434}
1435
1436#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
1437pub enum ImageSource {
1438 Asset {
1439 path: String,
1440 },
1441 File {
1442 path: String,
1443 },
1444 Network {
1445 url: String,
1446 #[serde(default)]
1447 headers: Vec<HttpHeader>,
1448 #[serde(default)]
1449 cache_policy: ImageCachePolicy,
1450 },
1451 Memory {
1452 bytes: Vec<u8>,
1453 #[serde(default)]
1454 mime_type: Option<String>,
1455 },
1456 SvgText {
1457 content: String,
1458 },
1459}
1460
1461impl Default for ImageSource {
1462 fn default() -> Self {
1463 Self::Asset {
1464 path: String::new(),
1465 }
1466 }
1467}
1468
1469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1470pub enum ImageLoadingBehavior {
1471 #[default]
1472 Empty,
1473 ThemePlaceholder,
1474 BlurHash(String),
1475}
1476
1477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1478pub enum ImageErrorBehavior {
1479 #[default]
1480 Empty,
1481 ThemeError,
1482 AltText,
1483}
1484
1485#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1486pub struct ImageRequest {
1487 pub source: ImageSource,
1488 #[serde(default)]
1489 pub cache_width: Option<u32>,
1490 #[serde(default)]
1491 pub cache_height: Option<u32>,
1492 #[serde(default)]
1493 pub semantic_label: Option<String>,
1494 #[serde(default)]
1495 pub loading: ImageLoadingBehavior,
1496 #[serde(default)]
1497 pub error: ImageErrorBehavior,
1498}
1499
1500impl ImageSource {
1501 pub fn stable_identity(&self) -> String {
1502 match self {
1503 Self::Asset { path } => format!("asset:{path}"),
1504 Self::File { path } => format!("file:{path}"),
1505 Self::Network {
1506 url,
1507 headers,
1508 cache_policy,
1509 } => {
1510 let mut identity = format!("network:{cache_policy:?}:{url}");
1511 for header in headers {
1512 identity.push('|');
1513 identity.push_str(&header.name.to_ascii_lowercase());
1514 identity.push('=');
1515 identity.push_str(&header.value);
1516 }
1517 identity
1518 }
1519 Self::Memory { bytes, mime_type } => {
1520 let digest = blake3::hash(bytes);
1521 format!("memory:{}:{digest}", mime_type.as_deref().unwrap_or(""))
1522 }
1523 Self::SvgText { content } => {
1524 let digest = blake3::hash(content.as_bytes());
1525 format!("svg:{digest}")
1526 }
1527 }
1528 }
1529
1530 pub fn local_path(&self) -> Option<&str> {
1531 match self {
1532 Self::Asset { path } | Self::File { path } => Some(path),
1533 _ => None,
1534 }
1535 }
1536
1537 pub fn network_url(&self) -> Option<&str> {
1538 match self {
1539 Self::Network { url, .. } => Some(url),
1540 _ => None,
1541 }
1542 }
1543}
1544
1545impl ImageRequest {
1546 pub fn stable_cache_key(&self) -> String {
1547 let mut hasher = blake3::Hasher::new();
1548 hasher.update(self.source.stable_identity().as_bytes());
1549 hasher.update(&self.cache_width.unwrap_or_default().to_le_bytes());
1550 hasher.update(&self.cache_height.unwrap_or_default().to_le_bytes());
1551 hasher.finalize().to_hex().to_string()
1552 }
1553}
1554
1555#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1556pub struct TextStyle {
1557 pub font_size: LayoutUnit,
1558 pub color: Color,
1559 pub underline: bool,
1560 #[serde(default)]
1561 pub font_family: Option<String>,
1562 #[serde(default)]
1563 pub locale: Option<String>,
1564 #[serde(default = "text_weight_default")]
1565 pub font_weight: u16,
1566 #[serde(default)]
1567 pub font_style: FontStyle,
1568 #[serde(default)]
1569 pub line_height: Option<LayoutUnit>,
1570 #[serde(default)]
1571 pub letter_spacing: LayoutUnit,
1572 pub background_color: Option<Color>,
1574}
1575
1576impl std::hash::Hash for TextStyle {
1577 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1578 self.font_size.to_bits().hash(state);
1579 self.color.hash(state);
1580 self.underline.hash(state);
1581 self.font_family.hash(state);
1582 self.locale.hash(state);
1583 self.font_weight.hash(state);
1584 self.font_style.hash(state);
1585 self.line_height.map(f32::to_bits).hash(state);
1586 self.letter_spacing.to_bits().hash(state);
1587 self.background_color.hash(state);
1588 }
1589}
1590
1591#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1592pub enum FontStyle {
1593 #[default]
1594 Normal,
1595 Italic,
1596}
1597
1598const fn text_weight_default() -> u16 {
1599 400
1600}
1601
1602#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
1603pub struct TextRun {
1604 pub text: String,
1605 pub style: TextStyle,
1606}
1607
1608#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
1609pub struct RichTextAnnotation {
1610 pub range: std::ops::Range<usize>,
1611 #[serde(default)]
1612 pub semantics_label: Option<String>,
1613 #[serde(default)]
1614 pub semantics_identifier: Option<String>,
1615 #[serde(default)]
1616 pub spell_out: Option<bool>,
1617 #[serde(default)]
1618 pub mouse_cursor: Option<MouseCursor>,
1619 #[serde(default)]
1620 pub actions: Vec<ActionEntry>,
1621}
1622
1623pub const INLINE_WIDGET_MARKER_PREFIX: &str = "__fission_inline_widget__:";
1624
1625#[derive(Debug, Clone, Copy, PartialEq)]
1626pub struct InlineWidgetMarker {
1627 pub id: u64,
1628 pub width: LayoutUnit,
1629 pub height: LayoutUnit,
1630}
1631
1632pub fn encode_inline_widget_marker(id: u64, width: LayoutUnit, height: LayoutUnit) -> String {
1633 format!("{INLINE_WIDGET_MARKER_PREFIX}{id}:{width}:{height}")
1634}
1635
1636pub fn decode_inline_widget_marker(family: Option<&str>) -> Option<InlineWidgetMarker> {
1637 let family = family?;
1638 let encoded = family.strip_prefix(INLINE_WIDGET_MARKER_PREFIX)?;
1639 let mut parts = encoded.split(':');
1640 let id = parts.next()?.parse().ok()?;
1641 let width = parts.next()?.parse().ok()?;
1642 let height = parts.next()?.parse().ok()?;
1643 if parts.next().is_some() {
1644 return None;
1645 }
1646 Some(InlineWidgetMarker { id, width, height })
1647}
1648
1649const fn text_wrap_default() -> bool {
1650 true
1651}
1652
1653#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1655pub enum BackdropFilter {
1656 Blur(LayoutUnit),
1658}
1659
1660impl std::hash::Hash for BackdropFilter {
1661 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1662 match self {
1663 Self::Blur(sigma) => {
1664 0_u8.hash(state);
1665 sigma.to_bits().hash(state);
1666 }
1667 }
1668 }
1669}
1670
1671#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1672pub enum PaintOp {
1673 BackdropFilter {
1674 filter: BackdropFilter,
1675 corner_radius: LayoutUnit,
1676 },
1677 DrawRect {
1678 fill: Option<Fill>,
1679 stroke: Option<Stroke>,
1680 corner_radius: LayoutUnit,
1681 shadow: Option<BoxShadow>,
1682 },
1683 DrawText {
1684 text: String,
1685 size: LayoutUnit,
1686 color: Color,
1687 underline: bool,
1688 #[serde(default = "text_wrap_default")]
1689 wrap: bool,
1690 caret_index: Option<usize>,
1691 #[serde(default)]
1692 caret_color: Option<Color>,
1693 #[serde(default)]
1694 caret_width: Option<LayoutUnit>,
1695 #[serde(default)]
1696 caret_height: Option<LayoutUnit>,
1697 #[serde(default)]
1698 caret_radius: Option<LayoutUnit>,
1699 #[serde(default)]
1700 paragraph_style: Option<TextParagraphStyle>,
1701 },
1702 DrawRichText {
1703 runs: Vec<TextRun>,
1704 #[serde(default = "text_wrap_default")]
1705 wrap: bool,
1706 caret_index: Option<usize>,
1707 #[serde(default)]
1708 caret_color: Option<Color>,
1709 #[serde(default)]
1710 caret_width: Option<LayoutUnit>,
1711 #[serde(default)]
1712 caret_height: Option<LayoutUnit>,
1713 #[serde(default)]
1714 caret_radius: Option<LayoutUnit>,
1715 #[serde(default)]
1716 paragraph_style: Option<TextParagraphStyle>,
1717 },
1718 DrawImage {
1719 request: ImageRequest,
1720 fit: ImageFit,
1721 alignment: ImageAlignment,
1722 },
1723 DrawPath {
1724 path: String,
1725 fill: Option<Fill>,
1726 stroke: Option<Stroke>,
1727 },
1728 DrawSvg {
1729 content: String,
1730 fill: Option<Fill>,
1731 stroke: Option<Stroke>,
1732 },
1733}
1734
1735impl std::hash::Hash for PaintOp {
1736 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1737 match self {
1738 Self::BackdropFilter {
1739 filter,
1740 corner_radius,
1741 } => {
1742 0_u8.hash(state);
1743 filter.hash(state);
1744 corner_radius.to_bits().hash(state);
1745 }
1746 Self::DrawRect {
1747 fill,
1748 stroke,
1749 corner_radius,
1750 shadow,
1751 } => {
1752 1_u8.hash(state);
1753 fill.hash(state);
1754 stroke.hash(state);
1755 corner_radius.to_bits().hash(state);
1756 shadow.hash(state);
1757 }
1758 Self::DrawText {
1759 text,
1760 size,
1761 color,
1762 underline,
1763 wrap,
1764 caret_index,
1765 caret_color,
1766 caret_width,
1767 caret_height,
1768 caret_radius,
1769 paragraph_style,
1770 } => {
1771 2_u8.hash(state);
1772 text.hash(state);
1773 size.to_bits().hash(state);
1774 color.hash(state);
1775 underline.hash(state);
1776 wrap.hash(state);
1777 caret_index.hash(state);
1778 caret_color.hash(state);
1779 caret_width.map(|w| w.to_bits()).hash(state);
1780 caret_height.map(|h| h.to_bits()).hash(state);
1781 caret_radius.map(|r| r.to_bits()).hash(state);
1782 paragraph_style.hash(state);
1783 }
1784 Self::DrawRichText {
1785 runs,
1786 wrap,
1787 caret_index,
1788 caret_color,
1789 caret_width,
1790 caret_height,
1791 caret_radius,
1792 paragraph_style,
1793 } => {
1794 3_u8.hash(state);
1795 runs.hash(state);
1796 wrap.hash(state);
1797 caret_index.hash(state);
1798 caret_color.hash(state);
1799 caret_width.map(|w| w.to_bits()).hash(state);
1800 caret_height.map(|h| h.to_bits()).hash(state);
1801 caret_radius.map(|r| r.to_bits()).hash(state);
1802 paragraph_style.hash(state);
1803 }
1804 Self::DrawImage {
1805 request,
1806 fit,
1807 alignment,
1808 } => {
1809 4_u8.hash(state);
1810 request.hash(state);
1811 fit.hash(state);
1812 alignment.hash(state);
1813 }
1814 Self::DrawPath { path, fill, stroke } => {
1815 5_u8.hash(state);
1816 path.hash(state);
1817 fill.hash(state);
1818 stroke.hash(state);
1819 }
1820 Self::DrawSvg {
1821 content,
1822 fill,
1823 stroke,
1824 } => {
1825 6_u8.hash(state);
1826 content.hash(state);
1827 fill.hash(state);
1828 stroke.hash(state);
1829 }
1830 }
1831 }
1832}
1833
1834#[cfg(test)]
1835mod tests {
1836 use super::{
1837 decode_inline_widget_marker, decode_text_paragraph_style, encode_inline_widget_marker,
1838 encode_text_paragraph_style, HttpHeader, ImageCachePolicy, ImageRequest, ImageSource,
1839 InlineWidgetMarker, TextAlign, TextDirection, TextHeightBehavior, TextOverflow,
1840 TextParagraphStyle, TextWidthBasis, TEXT_PARAGRAPH_MAX_ENCODED_LINES,
1841 };
1842
1843 #[test]
1844 fn paragraph_style_round_trips_alignment_overflow_and_line_cap() {
1845 let style = TextParagraphStyle {
1846 text_align: TextAlign::Justify,
1847 max_lines: Some(3),
1848 overflow: TextOverflow::Fade,
1849 text_direction: TextDirection::Auto,
1850 text_width_basis: TextWidthBasis::Parent,
1851 strut_line_height: None,
1852 text_height_behavior: TextHeightBehavior::default(),
1853 };
1854
1855 let encoded = encode_text_paragraph_style(style);
1856 assert_eq!(decode_text_paragraph_style(encoded), Some(style));
1857 }
1858
1859 #[test]
1860 fn paragraph_style_clamps_line_count_to_precise_encoding_budget() {
1861 let encoded = encode_text_paragraph_style(TextParagraphStyle {
1862 text_align: TextAlign::End,
1863 max_lines: Some(TEXT_PARAGRAPH_MAX_ENCODED_LINES + 99),
1864 overflow: TextOverflow::Ellipsis,
1865 text_direction: TextDirection::Auto,
1866 text_width_basis: TextWidthBasis::Parent,
1867 strut_line_height: None,
1868 text_height_behavior: TextHeightBehavior::default(),
1869 });
1870
1871 assert_eq!(
1872 decode_text_paragraph_style(encoded),
1873 Some(TextParagraphStyle {
1874 text_align: TextAlign::End,
1875 max_lines: Some(TEXT_PARAGRAPH_MAX_ENCODED_LINES),
1876 overflow: TextOverflow::Ellipsis,
1877 text_direction: TextDirection::Auto,
1878 text_width_basis: TextWidthBasis::Parent,
1879 strut_line_height: None,
1880 text_height_behavior: TextHeightBehavior::default(),
1881 })
1882 );
1883 }
1884
1885 #[test]
1886 fn image_request_cache_key_is_stable_and_dimension_sensitive() {
1887 let request = ImageRequest {
1888 source: ImageSource::Network {
1889 url: "https://cdn.example.com/image.webp".into(),
1890 headers: vec![HttpHeader {
1891 name: "Accept".into(),
1892 value: "image/webp".into(),
1893 }],
1894 cache_policy: ImageCachePolicy::Default,
1895 },
1896 cache_width: Some(320),
1897 cache_height: Some(180),
1898 ..Default::default()
1899 };
1900
1901 let same = request.clone();
1902 let mut resized = request.clone();
1903 resized.cache_width = Some(640);
1904
1905 assert_eq!(request.stable_cache_key(), same.stable_cache_key());
1906 assert_ne!(request.stable_cache_key(), resized.stable_cache_key());
1907 }
1908
1909 #[test]
1910 fn image_source_helpers_report_path_and_network_sources() {
1911 assert_eq!(
1912 ImageSource::Asset {
1913 path: "assets/logo.png".into()
1914 }
1915 .local_path(),
1916 Some("assets/logo.png")
1917 );
1918 assert_eq!(
1919 ImageSource::Network {
1920 url: "https://example.com/logo.png".into(),
1921 headers: Vec::new(),
1922 cache_policy: ImageCachePolicy::Default,
1923 }
1924 .network_url(),
1925 Some("https://example.com/logo.png")
1926 );
1927 }
1928
1929 #[test]
1930 fn paragraph_style_compact_encoding_rejects_extended_fields() {
1931 assert_eq!(
1932 encode_text_paragraph_style(TextParagraphStyle {
1933 text_align: TextAlign::Start,
1934 max_lines: Some(2),
1935 overflow: TextOverflow::Visible,
1936 text_direction: TextDirection::Rtl,
1937 text_width_basis: TextWidthBasis::LongestLine,
1938 strut_line_height: Some(24.0),
1939 text_height_behavior: TextHeightBehavior {
1940 apply_height_to_first_ascent: false,
1941 apply_height_to_last_descent: true,
1942 },
1943 }),
1944 None
1945 );
1946 }
1947
1948 #[test]
1949 fn inline_widget_marker_round_trips() {
1950 let encoded = encode_inline_widget_marker(7, 24.5, 12.0);
1951 assert_eq!(
1952 decode_inline_widget_marker(Some(encoded.as_str())),
1953 Some(InlineWidgetMarker {
1954 id: 7,
1955 width: 24.5,
1956 height: 12.0,
1957 })
1958 );
1959 }
1960}