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 Spotlight {
1036 anchor: WidgetId,
1037 padding: LayoutUnit,
1038 },
1039 Transform {
1040 transform: [f32; 16],
1041 },
1042 Clip {
1043 path: Option<String>,
1044 },
1045}
1046
1047impl std::hash::Hash for LayoutOp {
1048 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1049 let hash_unit = |u: LayoutUnit, h: &mut H| u.to_bits().hash(h);
1050 let hash_opt_unit = |u: Option<LayoutUnit>, h: &mut H| u.map(|v| v.to_bits()).hash(h);
1051 let hash_units = |us: [LayoutUnit; 4], h: &mut H| {
1052 for u in us {
1053 u.to_bits().hash(h);
1054 }
1055 };
1056
1057 match self {
1058 Self::Box {
1059 width,
1060 height,
1061 min_width,
1062 max_width,
1063 min_height,
1064 max_height,
1065 padding,
1066 flex_grow,
1067 flex_shrink,
1068 aspect_ratio,
1069 } => {
1070 0.hash(state);
1071 hash_opt_unit(*width, state);
1072 hash_opt_unit(*height, state);
1073 hash_opt_unit(*min_width, state);
1074 hash_opt_unit(*max_width, state);
1075 hash_opt_unit(*min_height, state);
1076 hash_opt_unit(*max_height, state);
1077 hash_units(*padding, state);
1078 hash_unit(*flex_grow, state);
1079 hash_unit(*flex_shrink, state);
1080 aspect_ratio.map(|f| f.to_bits()).hash(state);
1081 }
1082 Self::StyledBox {
1083 style,
1084 flex_grow,
1085 flex_shrink,
1086 } => {
1087 13.hash(state);
1088 style.hash(state);
1089 hash_unit(*flex_grow, state);
1090 hash_unit(*flex_shrink, state);
1091 }
1092 Self::Flex {
1093 direction,
1094 wrap,
1095 flex_grow,
1096 flex_shrink,
1097 padding,
1098 gap,
1099 align_items,
1100 justify_content,
1101 } => {
1102 1.hash(state);
1103 direction.hash(state);
1104 wrap.hash(state);
1105 hash_unit(*flex_grow, state);
1106 hash_unit(*flex_shrink, state);
1107 hash_units(*padding, state);
1108 hash_opt_unit(*gap, state);
1109 align_items.hash(state);
1110 justify_content.hash(state);
1111 }
1112 Self::Grid {
1113 columns,
1114 rows,
1115 column_gap,
1116 row_gap,
1117 padding,
1118 } => {
1119 2.hash(state);
1120 columns.hash(state);
1121 rows.hash(state);
1122 hash_opt_unit(*column_gap, state);
1123 hash_opt_unit(*row_gap, state);
1124 hash_units(*padding, state);
1125 }
1126 Self::GridItem {
1127 row_start,
1128 row_end,
1129 col_start,
1130 col_end,
1131 } => {
1132 3.hash(state);
1133 row_start.hash(state);
1134 row_end.hash(state);
1135 col_start.hash(state);
1136 col_end.hash(state);
1137 }
1138 Self::Responsive { query, cases } => {
1139 14.hash(state);
1140 query.hash(state);
1141 cases.hash(state);
1142 }
1143 Self::Scroll {
1144 direction,
1145 show_scrollbar,
1146 width,
1147 height,
1148 min_width,
1149 max_width,
1150 min_height,
1151 max_height,
1152 padding,
1153 flex_grow,
1154 flex_shrink,
1155 } => {
1156 4.hash(state);
1157 direction.hash(state);
1158 show_scrollbar.hash(state);
1159 hash_opt_unit(*width, state);
1160 hash_opt_unit(*height, state);
1161 hash_opt_unit(*min_width, state);
1162 hash_opt_unit(*max_width, state);
1163 hash_opt_unit(*min_height, state);
1164 hash_opt_unit(*max_height, state);
1165 hash_units(*padding, state);
1166 hash_unit(*flex_grow, state);
1167 hash_unit(*flex_shrink, state);
1168 }
1169 Self::Embed {
1170 kind,
1171 widget_id,
1172 width,
1173 height,
1174 } => {
1175 5.hash(state);
1176 kind.hash(state);
1177 widget_id.hash(state);
1178 hash_opt_unit(*width, state);
1179 hash_opt_unit(*height, state);
1180 }
1181 Self::AbsoluteFill => {
1182 6.hash(state);
1183 }
1184 Self::Positioned {
1185 left,
1186 top,
1187 right,
1188 bottom,
1189 width,
1190 height,
1191 } => {
1192 7.hash(state);
1193 hash_opt_unit(*left, state);
1194 hash_opt_unit(*top, state);
1195 hash_opt_unit(*right, state);
1196 hash_opt_unit(*bottom, state);
1197 hash_opt_unit(*width, state);
1198 hash_opt_unit(*height, state);
1199 }
1200 Self::PositionedLengths {
1201 left,
1202 top,
1203 right,
1204 bottom,
1205 width,
1206 height,
1207 } => {
1208 15.hash(state);
1209 left.hash(state);
1210 top.hash(state);
1211 right.hash(state);
1212 bottom.hash(state);
1213 width.hash(state);
1214 height.hash(state);
1215 }
1216 Self::ZStack => {
1217 8.hash(state);
1218 }
1219 Self::Align => {
1220 9.hash(state);
1221 }
1222 Self::Flyout { anchor, content } => {
1223 10.hash(state);
1224 anchor.hash(state);
1225 content.hash(state);
1226 }
1227 Self::Transform { transform } => {
1228 11.hash(state);
1229 for v in transform {
1230 v.to_bits().hash(state);
1231 }
1232 }
1233 Self::Clip { path } => {
1234 12.hash(state);
1235 path.hash(state);
1236 }
1237 Self::Spotlight { anchor, padding } => {
1238 16.hash(state);
1239 anchor.hash(state);
1240 hash_unit(*padding, state);
1241 }
1242 }
1243 }
1244}
1245
1246#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
1247pub struct Color {
1248 pub r: u8,
1249 pub g: u8,
1250 pub b: u8,
1251 pub a: u8,
1252}
1253
1254impl Color {
1255 pub const TRANSPARENT: Self = Self {
1256 r: 0,
1257 g: 0,
1258 b: 0,
1259 a: 0,
1260 };
1261 pub const BLACK: Self = Self {
1262 r: 0,
1263 g: 0,
1264 b: 0,
1265 a: 255,
1266 };
1267 pub const WHITE: Self = Self {
1268 r: 255,
1269 g: 255,
1270 b: 255,
1271 a: 255,
1272 };
1273 pub const RED: Self = Self {
1274 r: 255,
1275 g: 0,
1276 b: 0,
1277 a: 255,
1278 };
1279 pub const GREEN: Self = Self {
1280 r: 0,
1281 g: 255,
1282 b: 0,
1283 a: 255,
1284 };
1285 pub const BLUE: Self = Self {
1286 r: 0,
1287 g: 0,
1288 b: 255,
1289 a: 255,
1290 };
1291
1292 pub fn with_alpha(mut self, a: u8) -> Self {
1293 self.a = a;
1294 self
1295 }
1296}
1297
1298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1299pub enum Fill {
1300 Solid(Color),
1301 LinearGradient {
1305 start: (f32, f32),
1306 end: (f32, f32),
1307 stops: Vec<(f32, Color)>,
1308 },
1309 RadialGradient {
1311 center: (f32, f32),
1312 radius: f32,
1313 stops: Vec<(f32, Color)>,
1314 },
1315}
1316
1317impl std::hash::Hash for Fill {
1318 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1319 match self {
1320 Self::Solid(c) => {
1321 0.hash(state);
1322 c.hash(state);
1323 }
1324 Self::LinearGradient { start, end, stops } => {
1325 1.hash(state);
1326 start.0.to_bits().hash(state);
1327 start.1.to_bits().hash(state);
1328 end.0.to_bits().hash(state);
1329 end.1.to_bits().hash(state);
1330 for (off, c) in stops {
1331 off.to_bits().hash(state);
1332 c.hash(state);
1333 }
1334 }
1335 Self::RadialGradient {
1336 center,
1337 radius,
1338 stops,
1339 } => {
1340 2.hash(state);
1341 center.0.to_bits().hash(state);
1342 center.1.to_bits().hash(state);
1343 radius.to_bits().hash(state);
1344 for (off, c) in stops {
1345 off.to_bits().hash(state);
1346 c.hash(state);
1347 }
1348 }
1349 }
1350 }
1351}
1352
1353#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1354pub enum LineCap {
1355 Butt,
1356 Round,
1357 Square,
1358}
1359
1360#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1361pub enum LineJoin {
1362 Miter,
1363 Round,
1364 Bevel,
1365}
1366
1367#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1368pub struct Stroke {
1369 pub fill: Fill,
1370 pub width: LayoutUnit,
1371 pub dash_array: Option<Vec<f32>>,
1372 pub line_cap: LineCap,
1373 pub line_join: LineJoin,
1374}
1375
1376impl std::hash::Hash for Stroke {
1377 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1378 self.fill.hash(state);
1379 self.width.to_bits().hash(state);
1380 if let Some(da) = &self.dash_array {
1381 1.hash(state);
1382 for d in da {
1383 d.to_bits().hash(state);
1384 }
1385 } else {
1386 0.hash(state);
1387 }
1388 self.line_cap.hash(state);
1389 self.line_join.hash(state);
1390 }
1391}
1392
1393#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1394pub struct BoxShadow {
1395 pub color: Color,
1396 pub blur_radius: LayoutUnit,
1397 pub spread_radius: LayoutUnit,
1399 pub offset: (LayoutUnit, LayoutUnit),
1400 pub inset: bool,
1402}
1403
1404impl std::hash::Hash for BoxShadow {
1405 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1406 self.color.hash(state);
1407 self.blur_radius.to_bits().hash(state);
1408 self.spread_radius.to_bits().hash(state);
1409 self.offset.0.to_bits().hash(state);
1410 self.offset.1.to_bits().hash(state);
1411 self.inset.hash(state);
1412 }
1413}
1414
1415#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
1416pub enum ImageFit {
1417 Contain,
1418 Cover,
1419 Fill,
1420 None,
1421}
1422
1423#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1424pub enum ImageAlignment {
1425 TopStart,
1426 TopCenter,
1427 TopEnd,
1428 CenterStart,
1429 #[default]
1430 Center,
1431 CenterEnd,
1432 BottomStart,
1433 BottomCenter,
1434 BottomEnd,
1435}
1436
1437#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
1438pub struct HttpHeader {
1439 pub name: String,
1440 pub value: String,
1441}
1442
1443#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1444pub enum ImageCachePolicy {
1445 #[default]
1446 Default,
1447 Reload,
1448 MemoryOnly,
1449 Disk,
1450 NoStore,
1451}
1452
1453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
1454pub enum ImageSource {
1455 Asset {
1456 path: String,
1457 },
1458 File {
1459 path: String,
1460 },
1461 Network {
1462 url: String,
1463 #[serde(default)]
1464 headers: Vec<HttpHeader>,
1465 #[serde(default)]
1466 cache_policy: ImageCachePolicy,
1467 },
1468 Memory {
1469 bytes: Vec<u8>,
1470 #[serde(default)]
1471 mime_type: Option<String>,
1472 },
1473 SvgText {
1474 content: String,
1475 },
1476}
1477
1478impl Default for ImageSource {
1479 fn default() -> Self {
1480 Self::Asset {
1481 path: String::new(),
1482 }
1483 }
1484}
1485
1486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1487pub enum ImageLoadingBehavior {
1488 #[default]
1489 Empty,
1490 ThemePlaceholder,
1491 BlurHash(String),
1492}
1493
1494#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1495pub enum ImageErrorBehavior {
1496 #[default]
1497 Empty,
1498 ThemeError,
1499 AltText,
1500}
1501
1502#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1503pub struct ImageRequest {
1504 pub source: ImageSource,
1505 #[serde(default)]
1506 pub cache_width: Option<u32>,
1507 #[serde(default)]
1508 pub cache_height: Option<u32>,
1509 #[serde(default)]
1510 pub semantic_label: Option<String>,
1511 #[serde(default)]
1512 pub loading: ImageLoadingBehavior,
1513 #[serde(default)]
1514 pub error: ImageErrorBehavior,
1515}
1516
1517impl ImageSource {
1518 pub fn stable_identity(&self) -> String {
1519 match self {
1520 Self::Asset { path } => format!("asset:{path}"),
1521 Self::File { path } => format!("file:{path}"),
1522 Self::Network {
1523 url,
1524 headers,
1525 cache_policy,
1526 } => {
1527 let mut identity = format!("network:{cache_policy:?}:{url}");
1528 for header in headers {
1529 identity.push('|');
1530 identity.push_str(&header.name.to_ascii_lowercase());
1531 identity.push('=');
1532 identity.push_str(&header.value);
1533 }
1534 identity
1535 }
1536 Self::Memory { bytes, mime_type } => {
1537 let digest = blake3::hash(bytes);
1538 format!("memory:{}:{digest}", mime_type.as_deref().unwrap_or(""))
1539 }
1540 Self::SvgText { content } => {
1541 let digest = blake3::hash(content.as_bytes());
1542 format!("svg:{digest}")
1543 }
1544 }
1545 }
1546
1547 pub fn local_path(&self) -> Option<&str> {
1548 match self {
1549 Self::Asset { path } | Self::File { path } => Some(path),
1550 _ => None,
1551 }
1552 }
1553
1554 pub fn network_url(&self) -> Option<&str> {
1555 match self {
1556 Self::Network { url, .. } => Some(url),
1557 _ => None,
1558 }
1559 }
1560}
1561
1562impl ImageRequest {
1563 pub fn stable_cache_key(&self) -> String {
1564 let mut hasher = blake3::Hasher::new();
1565 hasher.update(self.source.stable_identity().as_bytes());
1566 hasher.update(&self.cache_width.unwrap_or_default().to_le_bytes());
1567 hasher.update(&self.cache_height.unwrap_or_default().to_le_bytes());
1568 hasher.finalize().to_hex().to_string()
1569 }
1570}
1571
1572#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1573pub struct TextStyle {
1574 pub font_size: LayoutUnit,
1575 pub color: Color,
1576 pub underline: bool,
1577 #[serde(default)]
1578 pub font_family: Option<String>,
1579 #[serde(default)]
1580 pub locale: Option<String>,
1581 #[serde(default = "text_weight_default")]
1582 pub font_weight: u16,
1583 #[serde(default)]
1584 pub font_style: FontStyle,
1585 #[serde(default)]
1586 pub line_height: Option<LayoutUnit>,
1587 #[serde(default)]
1588 pub letter_spacing: LayoutUnit,
1589 pub background_color: Option<Color>,
1591}
1592
1593impl std::hash::Hash for TextStyle {
1594 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1595 self.font_size.to_bits().hash(state);
1596 self.color.hash(state);
1597 self.underline.hash(state);
1598 self.font_family.hash(state);
1599 self.locale.hash(state);
1600 self.font_weight.hash(state);
1601 self.font_style.hash(state);
1602 self.line_height.map(f32::to_bits).hash(state);
1603 self.letter_spacing.to_bits().hash(state);
1604 self.background_color.hash(state);
1605 }
1606}
1607
1608#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1609pub enum FontStyle {
1610 #[default]
1611 Normal,
1612 Italic,
1613}
1614
1615const fn text_weight_default() -> u16 {
1616 400
1617}
1618
1619#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
1620pub struct TextRun {
1621 pub text: String,
1622 pub style: TextStyle,
1623}
1624
1625#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
1626pub struct RichTextAnnotation {
1627 pub range: std::ops::Range<usize>,
1628 #[serde(default)]
1629 pub semantics_label: Option<String>,
1630 #[serde(default)]
1631 pub semantics_identifier: Option<String>,
1632 #[serde(default)]
1633 pub spell_out: Option<bool>,
1634 #[serde(default)]
1635 pub mouse_cursor: Option<MouseCursor>,
1636 #[serde(default)]
1637 pub actions: Vec<ActionEntry>,
1638}
1639
1640pub const INLINE_WIDGET_MARKER_PREFIX: &str = "__fission_inline_widget__:";
1641
1642#[derive(Debug, Clone, Copy, PartialEq)]
1643pub struct InlineWidgetMarker {
1644 pub id: u64,
1645 pub width: LayoutUnit,
1646 pub height: LayoutUnit,
1647}
1648
1649pub fn encode_inline_widget_marker(id: u64, width: LayoutUnit, height: LayoutUnit) -> String {
1650 format!("{INLINE_WIDGET_MARKER_PREFIX}{id}:{width}:{height}")
1651}
1652
1653pub fn decode_inline_widget_marker(family: Option<&str>) -> Option<InlineWidgetMarker> {
1654 let family = family?;
1655 let encoded = family.strip_prefix(INLINE_WIDGET_MARKER_PREFIX)?;
1656 let mut parts = encoded.split(':');
1657 let id = parts.next()?.parse().ok()?;
1658 let width = parts.next()?.parse().ok()?;
1659 let height = parts.next()?.parse().ok()?;
1660 if parts.next().is_some() {
1661 return None;
1662 }
1663 Some(InlineWidgetMarker { id, width, height })
1664}
1665
1666const fn text_wrap_default() -> bool {
1667 true
1668}
1669
1670#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1672pub enum BackdropFilter {
1673 Blur(LayoutUnit),
1675}
1676
1677impl std::hash::Hash for BackdropFilter {
1678 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1679 match self {
1680 Self::Blur(sigma) => {
1681 0_u8.hash(state);
1682 sigma.to_bits().hash(state);
1683 }
1684 }
1685 }
1686}
1687
1688#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1689pub enum PaintOp {
1690 BackdropFilter {
1691 filter: BackdropFilter,
1692 corner_radius: LayoutUnit,
1693 },
1694 DrawRect {
1695 fill: Option<Fill>,
1696 stroke: Option<Stroke>,
1697 corner_radius: LayoutUnit,
1698 shadow: Option<BoxShadow>,
1699 },
1700 DrawText {
1701 text: String,
1702 size: LayoutUnit,
1703 color: Color,
1704 underline: bool,
1705 #[serde(default = "text_wrap_default")]
1706 wrap: bool,
1707 caret_index: Option<usize>,
1708 #[serde(default)]
1709 caret_color: Option<Color>,
1710 #[serde(default)]
1711 caret_width: Option<LayoutUnit>,
1712 #[serde(default)]
1713 caret_height: Option<LayoutUnit>,
1714 #[serde(default)]
1715 caret_radius: Option<LayoutUnit>,
1716 #[serde(default)]
1717 paragraph_style: Option<TextParagraphStyle>,
1718 },
1719 DrawRichText {
1720 runs: Vec<TextRun>,
1721 #[serde(default = "text_wrap_default")]
1722 wrap: bool,
1723 caret_index: Option<usize>,
1724 #[serde(default)]
1725 caret_color: Option<Color>,
1726 #[serde(default)]
1727 caret_width: Option<LayoutUnit>,
1728 #[serde(default)]
1729 caret_height: Option<LayoutUnit>,
1730 #[serde(default)]
1731 caret_radius: Option<LayoutUnit>,
1732 #[serde(default)]
1733 paragraph_style: Option<TextParagraphStyle>,
1734 },
1735 DrawImage {
1736 request: ImageRequest,
1737 fit: ImageFit,
1738 alignment: ImageAlignment,
1739 },
1740 DrawPath {
1741 path: String,
1742 fill: Option<Fill>,
1743 stroke: Option<Stroke>,
1744 },
1745 DrawSvg {
1746 content: String,
1747 fill: Option<Fill>,
1748 stroke: Option<Stroke>,
1749 },
1750}
1751
1752impl std::hash::Hash for PaintOp {
1753 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1754 match self {
1755 Self::BackdropFilter {
1756 filter,
1757 corner_radius,
1758 } => {
1759 0_u8.hash(state);
1760 filter.hash(state);
1761 corner_radius.to_bits().hash(state);
1762 }
1763 Self::DrawRect {
1764 fill,
1765 stroke,
1766 corner_radius,
1767 shadow,
1768 } => {
1769 1_u8.hash(state);
1770 fill.hash(state);
1771 stroke.hash(state);
1772 corner_radius.to_bits().hash(state);
1773 shadow.hash(state);
1774 }
1775 Self::DrawText {
1776 text,
1777 size,
1778 color,
1779 underline,
1780 wrap,
1781 caret_index,
1782 caret_color,
1783 caret_width,
1784 caret_height,
1785 caret_radius,
1786 paragraph_style,
1787 } => {
1788 2_u8.hash(state);
1789 text.hash(state);
1790 size.to_bits().hash(state);
1791 color.hash(state);
1792 underline.hash(state);
1793 wrap.hash(state);
1794 caret_index.hash(state);
1795 caret_color.hash(state);
1796 caret_width.map(|w| w.to_bits()).hash(state);
1797 caret_height.map(|h| h.to_bits()).hash(state);
1798 caret_radius.map(|r| r.to_bits()).hash(state);
1799 paragraph_style.hash(state);
1800 }
1801 Self::DrawRichText {
1802 runs,
1803 wrap,
1804 caret_index,
1805 caret_color,
1806 caret_width,
1807 caret_height,
1808 caret_radius,
1809 paragraph_style,
1810 } => {
1811 3_u8.hash(state);
1812 runs.hash(state);
1813 wrap.hash(state);
1814 caret_index.hash(state);
1815 caret_color.hash(state);
1816 caret_width.map(|w| w.to_bits()).hash(state);
1817 caret_height.map(|h| h.to_bits()).hash(state);
1818 caret_radius.map(|r| r.to_bits()).hash(state);
1819 paragraph_style.hash(state);
1820 }
1821 Self::DrawImage {
1822 request,
1823 fit,
1824 alignment,
1825 } => {
1826 4_u8.hash(state);
1827 request.hash(state);
1828 fit.hash(state);
1829 alignment.hash(state);
1830 }
1831 Self::DrawPath { path, fill, stroke } => {
1832 5_u8.hash(state);
1833 path.hash(state);
1834 fill.hash(state);
1835 stroke.hash(state);
1836 }
1837 Self::DrawSvg {
1838 content,
1839 fill,
1840 stroke,
1841 } => {
1842 6_u8.hash(state);
1843 content.hash(state);
1844 fill.hash(state);
1845 stroke.hash(state);
1846 }
1847 }
1848 }
1849}
1850
1851#[cfg(test)]
1852mod tests {
1853 use super::{
1854 decode_inline_widget_marker, decode_text_paragraph_style, encode_inline_widget_marker,
1855 encode_text_paragraph_style, HttpHeader, ImageCachePolicy, ImageRequest, ImageSource,
1856 InlineWidgetMarker, TextAlign, TextDirection, TextHeightBehavior, TextOverflow,
1857 TextParagraphStyle, TextWidthBasis, TEXT_PARAGRAPH_MAX_ENCODED_LINES,
1858 };
1859
1860 #[test]
1861 fn paragraph_style_round_trips_alignment_overflow_and_line_cap() {
1862 let style = TextParagraphStyle {
1863 text_align: TextAlign::Justify,
1864 max_lines: Some(3),
1865 overflow: TextOverflow::Fade,
1866 text_direction: TextDirection::Auto,
1867 text_width_basis: TextWidthBasis::Parent,
1868 strut_line_height: None,
1869 text_height_behavior: TextHeightBehavior::default(),
1870 };
1871
1872 let encoded = encode_text_paragraph_style(style);
1873 assert_eq!(decode_text_paragraph_style(encoded), Some(style));
1874 }
1875
1876 #[test]
1877 fn paragraph_style_clamps_line_count_to_precise_encoding_budget() {
1878 let encoded = encode_text_paragraph_style(TextParagraphStyle {
1879 text_align: TextAlign::End,
1880 max_lines: Some(TEXT_PARAGRAPH_MAX_ENCODED_LINES + 99),
1881 overflow: TextOverflow::Ellipsis,
1882 text_direction: TextDirection::Auto,
1883 text_width_basis: TextWidthBasis::Parent,
1884 strut_line_height: None,
1885 text_height_behavior: TextHeightBehavior::default(),
1886 });
1887
1888 assert_eq!(
1889 decode_text_paragraph_style(encoded),
1890 Some(TextParagraphStyle {
1891 text_align: TextAlign::End,
1892 max_lines: Some(TEXT_PARAGRAPH_MAX_ENCODED_LINES),
1893 overflow: TextOverflow::Ellipsis,
1894 text_direction: TextDirection::Auto,
1895 text_width_basis: TextWidthBasis::Parent,
1896 strut_line_height: None,
1897 text_height_behavior: TextHeightBehavior::default(),
1898 })
1899 );
1900 }
1901
1902 #[test]
1903 fn image_request_cache_key_is_stable_and_dimension_sensitive() {
1904 let request = ImageRequest {
1905 source: ImageSource::Network {
1906 url: "https://cdn.example.com/image.webp".into(),
1907 headers: vec![HttpHeader {
1908 name: "Accept".into(),
1909 value: "image/webp".into(),
1910 }],
1911 cache_policy: ImageCachePolicy::Default,
1912 },
1913 cache_width: Some(320),
1914 cache_height: Some(180),
1915 ..Default::default()
1916 };
1917
1918 let same = request.clone();
1919 let mut resized = request.clone();
1920 resized.cache_width = Some(640);
1921
1922 assert_eq!(request.stable_cache_key(), same.stable_cache_key());
1923 assert_ne!(request.stable_cache_key(), resized.stable_cache_key());
1924 }
1925
1926 #[test]
1927 fn image_source_helpers_report_path_and_network_sources() {
1928 assert_eq!(
1929 ImageSource::Asset {
1930 path: "assets/logo.png".into()
1931 }
1932 .local_path(),
1933 Some("assets/logo.png")
1934 );
1935 assert_eq!(
1936 ImageSource::Network {
1937 url: "https://example.com/logo.png".into(),
1938 headers: Vec::new(),
1939 cache_policy: ImageCachePolicy::Default,
1940 }
1941 .network_url(),
1942 Some("https://example.com/logo.png")
1943 );
1944 }
1945
1946 #[test]
1947 fn paragraph_style_compact_encoding_rejects_extended_fields() {
1948 assert_eq!(
1949 encode_text_paragraph_style(TextParagraphStyle {
1950 text_align: TextAlign::Start,
1951 max_lines: Some(2),
1952 overflow: TextOverflow::Visible,
1953 text_direction: TextDirection::Rtl,
1954 text_width_basis: TextWidthBasis::LongestLine,
1955 strut_line_height: Some(24.0),
1956 text_height_behavior: TextHeightBehavior {
1957 apply_height_to_first_ascent: false,
1958 apply_height_to_last_descent: true,
1959 },
1960 }),
1961 None
1962 );
1963 }
1964
1965 #[test]
1966 fn inline_widget_marker_round_trips() {
1967 let encoded = encode_inline_widget_marker(7, 24.5, 12.0);
1968 assert_eq!(
1969 decode_inline_widget_marker(Some(encoded.as_str())),
1970 Some(InlineWidgetMarker {
1971 id: 7,
1972 width: 24.5,
1973 height: 12.0,
1974 })
1975 );
1976 }
1977}