Skip to main content

fission_ir/
op.rs

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