Skip to main content

fission_ir/
op.rs

1use super::semantics::{ActionEntry, Semantics};
2pub use crate::viewport::{
3    ViewportBoundary, ViewportClip, ViewportMargin, ViewportPanAxis, ViewportTransform,
4    ViewportZoomPolicy,
5};
6use crate::WidgetId;
7use serde::{Deserialize, Serialize};
8
9// The fundamental operations that can be performed in the Core IR.
10// These are low-level, platform-agnostic, and deterministic.
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub enum Op {
13    Structural(StructuralOp),
14    Layout(LayoutOp),
15    Paint(PaintOp),
16    Semantics(Semantics),
17}
18
19impl std::hash::Hash for Op {
20    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
21        match self {
22            Self::Structural(s) => {
23                0.hash(state);
24                s.hash(state);
25            }
26            Self::Layout(l) => {
27                1.hash(state);
28                l.hash(state);
29            }
30            Self::Paint(p) => {
31                2.hash(state);
32                p.hash(state);
33            }
34            Self::Semantics(s) => {
35                3.hash(state);
36                s.hash(state);
37            }
38        }
39    }
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
43pub enum StructuralOp {
44    Group { stable_hash: u64 },
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
48pub struct CompositeScalar {
49    pub base: f32,
50    pub motion_target: Option<WidgetId>,
51}
52
53impl CompositeScalar {
54    pub fn new(base: f32) -> Self {
55        Self {
56            base,
57            motion_target: None,
58        }
59    }
60
61    pub fn motion(mut self, target: WidgetId) -> Self {
62        self.motion_target = Some(target);
63        self
64    }
65}
66
67impl std::hash::Hash for CompositeScalar {
68    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
69        self.base.to_bits().hash(state);
70        self.motion_target.hash(state);
71    }
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash, Default)]
75pub struct CompositeStyle {
76    pub opacity: Option<CompositeScalar>,
77    pub translate_x: Option<CompositeScalar>,
78    pub translate_y: Option<CompositeScalar>,
79    pub scale: Option<CompositeScalar>,
80    pub rotation: Option<CompositeScalar>,
81    pub clip_to_bounds: bool,
82    pub repaint_boundary: bool,
83}
84
85pub type LayoutUnit = f32;
86
87/// A declarative layout length resolved by the constraint engine.
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89pub enum Length {
90    /// Fixed logical points.
91    Points(LayoutUnit),
92    /// Percentage of the containing axis. `50.0` means 50%, not `0.5`.
93    Percent(f32),
94    /// Percentage of the active viewport width. `100.0` means full viewport width.
95    ViewportWidth(f32),
96    /// Percentage of the active viewport height. `100.0` means full viewport height.
97    ViewportHeight(f32),
98    /// Sum of two length expressions.
99    Add(Box<Length>, Box<Length>),
100    /// Difference between two length expressions.
101    Subtract(Box<Length>, Box<Length>),
102    /// Smallest value from a list of fully resolvable length expressions.
103    Min(Vec<Length>),
104    /// Largest value from a list of fully resolvable length expressions.
105    Max(Vec<Length>),
106    /// Preferred value clamped between lower and upper bounds.
107    Clamp {
108        /// Lower bound.
109        min: Box<Length>,
110        /// Preferred value before clamping.
111        preferred: Box<Length>,
112        /// Upper bound.
113        max: Box<Length>,
114    },
115    /// Size to intrinsic content, optionally capped by a limit.
116    FitContent(Option<Box<Length>>),
117    /// Minimum intrinsic size required by the content.
118    MinContent,
119    /// Preferred intrinsic size of the content without wrapping.
120    MaxContent,
121    /// Let the active layout algorithm choose the size.
122    Auto,
123}
124
125impl Length {
126    /// Creates a fixed logical-point length.
127    pub fn points(value: LayoutUnit) -> Self {
128        Self::Points(value)
129    }
130
131    /// Creates a percentage of the containing axis.
132    pub fn percent(value: f32) -> Self {
133        Self::Percent(value)
134    }
135
136    /// Creates a percentage of the viewport width.
137    pub fn vw(value: f32) -> Self {
138        Self::ViewportWidth(value)
139    }
140
141    /// Creates a percentage of the viewport height.
142    pub fn vh(value: f32) -> Self {
143        Self::ViewportHeight(value)
144    }
145
146    /// Clamps a preferred length between lower and upper bounds.
147    pub fn clamp(min: Length, preferred: Length, max: Length) -> Self {
148        Self::Clamp {
149            min: Box::new(min),
150            preferred: Box::new(preferred),
151            max: Box::new(max),
152        }
153    }
154
155    /// Selects the smallest fully resolved length.
156    pub fn min(values: impl Into<Vec<Length>>) -> Self {
157        Self::Min(values.into())
158    }
159
160    /// Selects the largest fully resolved length.
161    pub fn max(values: impl Into<Vec<Length>>) -> Self {
162        Self::Max(values.into())
163    }
164
165    /// Sizes to content, optionally capped by a resolved limit.
166    pub fn fit_content(limit: impl Into<Option<Length>>) -> Self {
167        Self::FitContent(limit.into().map(Box::new))
168    }
169
170    /// Creates `[left, right, top, bottom]` edges with one shared value.
171    pub fn all(value: Length) -> [Length; 4] {
172        std::array::from_fn(|_| value.clone())
173    }
174
175    /// Creates `[left, right, top, bottom]` edges from axis values.
176    pub fn symmetric(horizontal: Length, vertical: Length) -> [Length; 4] {
177        [horizontal.clone(), horizontal, vertical.clone(), vertical]
178    }
179
180    /// Resolves a numeric length against one axis and the active viewport.
181    ///
182    /// Intrinsic and automatic lengths return `None` because they require a
183    /// layout measurement rather than arithmetic resolution.
184    pub fn resolve(
185        &self,
186        reference: LayoutUnit,
187        viewport_width: LayoutUnit,
188        viewport_height: LayoutUnit,
189    ) -> Option<LayoutUnit> {
190        let resolved = match self {
191            Self::Points(value) => *value,
192            Self::Percent(value) => reference.is_finite().then_some(reference * value / 100.0)?,
193            Self::ViewportWidth(value) => viewport_width * value / 100.0,
194            Self::ViewportHeight(value) => viewport_height * value / 100.0,
195            Self::Add(left, right) => {
196                left.resolve(reference, viewport_width, viewport_height)?
197                    + right.resolve(reference, viewport_width, viewport_height)?
198            }
199            Self::Subtract(left, right) => {
200                left.resolve(reference, viewport_width, viewport_height)?
201                    - right.resolve(reference, viewport_width, viewport_height)?
202            }
203            Self::Min(values) => resolve_length_list(
204                values,
205                reference,
206                viewport_width,
207                viewport_height,
208                LayoutUnit::min,
209            )?,
210            Self::Max(values) => resolve_length_list(
211                values,
212                reference,
213                viewport_width,
214                viewport_height,
215                LayoutUnit::max,
216            )?,
217            Self::Clamp {
218                min,
219                preferred,
220                max,
221            } => {
222                let minimum = min.resolve(reference, viewport_width, viewport_height)?;
223                let maximum = max.resolve(reference, viewport_width, viewport_height)?;
224                preferred
225                    .resolve(reference, viewport_width, viewport_height)?
226                    .clamp(minimum.min(maximum), minimum.max(maximum))
227            }
228            Self::FitContent(_) | Self::MinContent | Self::MaxContent | Self::Auto => return None,
229        };
230        resolved.is_finite().then_some(resolved)
231    }
232}
233
234fn resolve_length_list(
235    values: &[Length],
236    reference: LayoutUnit,
237    viewport_width: LayoutUnit,
238    viewport_height: LayoutUnit,
239    combine: impl Fn(LayoutUnit, LayoutUnit) -> LayoutUnit,
240) -> Option<LayoutUnit> {
241    let mut values = values.iter();
242    let mut resolved = values
243        .next()?
244        .resolve(reference, viewport_width, viewport_height)?;
245    for value in values {
246        resolved = combine(
247            resolved,
248            value.resolve(reference, viewport_width, viewport_height)?,
249        );
250    }
251    Some(resolved)
252}
253
254impl From<LayoutUnit> for Length {
255    fn from(value: LayoutUnit) -> Self {
256        Self::Points(value)
257    }
258}
259
260impl std::ops::Add for Length {
261    type Output = Self;
262
263    fn add(self, rhs: Self) -> Self::Output {
264        Self::Add(Box::new(self), Box::new(rhs))
265    }
266}
267
268impl std::ops::Sub for Length {
269    type Output = Self;
270
271    fn sub(self, rhs: Self) -> Self::Output {
272        Self::Subtract(Box::new(self), Box::new(rhs))
273    }
274}
275
276impl std::hash::Hash for Length {
277    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
278        std::mem::discriminant(self).hash(state);
279        match self {
280            Self::Points(value)
281            | Self::Percent(value)
282            | Self::ViewportWidth(value)
283            | Self::ViewportHeight(value) => value.to_bits().hash(state),
284            Self::Add(left, right) | Self::Subtract(left, right) => {
285                left.hash(state);
286                right.hash(state);
287            }
288            Self::Min(values) | Self::Max(values) => values.hash(state),
289            Self::Clamp {
290                min,
291                preferred,
292                max,
293            } => {
294                min.hash(state);
295                preferred.hash(state);
296                max.hash(state);
297            }
298            Self::FitContent(limit) => limit.hash(state),
299            Self::MinContent | Self::MaxContent | Self::Auto => {}
300        }
301    }
302}
303
304/// Overflow behavior for a common box.
305#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
306pub enum Overflow {
307    /// Let content paint outside the box's assigned rectangle.
308    #[default]
309    Visible,
310    /// Clip content to the box's assigned rectangle.
311    Clip,
312}
313
314/// Alignment of a box's child within its content rectangle.
315#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
316pub enum BoxAlignment {
317    /// Place the child at the start of both axes.
318    #[default]
319    Start,
320    /// Center the child on both axes.
321    Center,
322    /// Place the child at the end of both axes.
323    End,
324    /// Stretch the child to the content rectangle where the child has no explicit size.
325    Stretch,
326}
327
328/// Absolute positioning values for a common box.
329#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, Hash)]
330pub struct BoxPosition {
331    /// Distance from the parent's left edge.
332    pub left: Option<Length>,
333    /// Distance from the parent's top edge.
334    pub top: Option<Length>,
335    /// Distance from the parent's right edge.
336    pub right: Option<Length>,
337    /// Distance from the parent's bottom edge.
338    pub bottom: Option<Length>,
339}
340
341/// Grid placement values for a common box.
342#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
343pub struct BoxGridPlacement {
344    /// Starting row line or automatic placement.
345    pub row_start: GridPlacement,
346    /// Ending row line, span, or automatic placement.
347    pub row_end: GridPlacement,
348    /// Starting column line or automatic placement.
349    pub col_start: GridPlacement,
350    /// Ending column line, span, or automatic placement.
351    pub col_end: GridPlacement,
352}
353
354/// Typed sizing and overflow shared by common box-like widgets.
355///
356/// `BoxStyle` lets widgets expose CSS-like layout capabilities without
357/// embedding CSS or shell-specific behavior in application code.
358///
359/// # Example
360///
361/// ```rust
362/// use fission_ir::op::{BoxAlignment, BoxStyle, Length, Overflow};
363///
364/// let style = BoxStyle::default()
365///     .width(Length::clamp(
366///         Length::points(280.0),
367///         Length::percent(50.0),
368///         Length::points(720.0),
369///     ))
370///     .padding_symmetric(Length::points(24.0), Length::points(16.0))
371///     .align(BoxAlignment::Center)
372///     .overflow(Overflow::Clip);
373/// ```
374#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, Hash)]
375pub struct BoxStyle {
376    /// Preferred width.
377    pub width: Option<Length>,
378    /// Preferred height.
379    pub height: Option<Length>,
380    /// Minimum width constraint.
381    pub min_width: Option<Length>,
382    /// Maximum width constraint.
383    pub max_width: Option<Length>,
384    /// Minimum height constraint.
385    pub min_height: Option<Length>,
386    /// Maximum height constraint.
387    pub max_height: Option<Length>,
388    /// Inner spacing in `[left, right, top, bottom]` order.
389    pub padding: Option<[Length; 4]>,
390    /// Outer spacing in `[left, right, top, bottom]` order.
391    pub margin: Option<[Length; 4]>,
392    /// Width-to-height ratio.
393    pub aspect_ratio: Option<OrderedLayoutUnit>,
394    /// Whether content can paint outside this box.
395    pub overflow: Overflow,
396    /// Child alignment inside the content rectangle.
397    pub alignment: BoxAlignment,
398    /// Optional absolute positioning offsets.
399    pub position: Option<BoxPosition>,
400    /// Optional parent-grid placement.
401    pub grid: Option<BoxGridPlacement>,
402    /// Flex grow participation for box-like widgets.
403    pub flex_grow: Option<OrderedLayoutUnit>,
404    /// Flex shrink participation for box-like widgets.
405    pub flex_shrink: Option<OrderedLayoutUnit>,
406}
407
408impl BoxStyle {
409    /// Sets the preferred width.
410    pub fn width(mut self, value: Length) -> Self {
411        self.width = Some(value);
412        self
413    }
414
415    /// Sets the preferred height.
416    pub fn height(mut self, value: Length) -> Self {
417        self.height = Some(value);
418        self
419    }
420
421    /// Sets the minimum width.
422    pub fn min_width(mut self, value: Length) -> Self {
423        self.min_width = Some(value);
424        self
425    }
426
427    /// Sets the maximum width.
428    pub fn max_width(mut self, value: Length) -> Self {
429        self.max_width = Some(value);
430        self
431    }
432
433    /// Sets the minimum height.
434    pub fn min_height(mut self, value: Length) -> Self {
435        self.min_height = Some(value);
436        self
437    }
438
439    /// Sets the maximum height.
440    pub fn max_height(mut self, value: Length) -> Self {
441        self.max_height = Some(value);
442        self
443    }
444
445    /// Sets `[left, right, top, bottom]` inner spacing.
446    pub fn padding(mut self, edges: [Length; 4]) -> Self {
447        self.padding = Some(edges);
448        self
449    }
450
451    /// Sets equal inner spacing on every edge.
452    pub fn padding_all(self, value: Length) -> Self {
453        self.padding(Length::all(value))
454    }
455
456    /// Sets horizontal and vertical inner spacing.
457    pub fn padding_symmetric(self, horizontal: Length, vertical: Length) -> Self {
458        self.padding(Length::symmetric(horizontal, vertical))
459    }
460
461    /// Sets `[left, right, top, bottom]` outer spacing.
462    pub fn margin(mut self, edges: [Length; 4]) -> Self {
463        self.margin = Some(edges);
464        self
465    }
466
467    /// Sets equal outer spacing on every edge.
468    pub fn margin_all(self, value: Length) -> Self {
469        self.margin(Length::all(value))
470    }
471
472    /// Sets horizontal and vertical outer spacing.
473    pub fn margin_symmetric(self, horizontal: Length, vertical: Length) -> Self {
474        self.margin(Length::symmetric(horizontal, vertical))
475    }
476
477    /// Sets overflow visibility or clipping.
478    pub fn overflow(mut self, overflow: Overflow) -> Self {
479        self.overflow = overflow;
480        self
481    }
482
483    /// Aligns the child within the box's content rectangle.
484    pub fn align(mut self, alignment: BoxAlignment) -> Self {
485        self.alignment = alignment;
486        self
487    }
488
489    /// Sets a non-negative width-to-height ratio.
490    pub fn aspect_ratio(mut self, ratio: LayoutUnit) -> Self {
491        self.aspect_ratio = Some(OrderedLayoutUnit(ratio.max(0.0)));
492        self
493    }
494
495    /// Absolutely positions the box within its positioned parent.
496    pub fn positioned(mut self, position: BoxPosition) -> Self {
497        self.position = Some(position);
498        self
499    }
500
501    /// Places the box in a parent grid.
502    pub fn grid(mut self, placement: BoxGridPlacement) -> Self {
503        self.grid = Some(placement);
504        self
505    }
506
507    /// Sets flex grow and shrink participation.
508    pub fn flex(mut self, grow: LayoutUnit, shrink: LayoutUnit) -> Self {
509        self.flex_grow = Some(OrderedLayoutUnit(grow));
510        self.flex_shrink = Some(OrderedLayoutUnit(shrink));
511        self
512    }
513}
514
515/// Hashable/serializable wrapper for floating-point layout values.
516#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
517pub struct OrderedLayoutUnit(
518    /// Wrapped finite layout value.
519    pub LayoutUnit,
520);
521
522impl std::hash::Hash for OrderedLayoutUnit {
523    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
524        self.0.to_bits().hash(state);
525    }
526}
527
528#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
529pub enum TextAlign {
530    Left,
531    Right,
532    Center,
533    Justify,
534    #[default]
535    Start,
536    End,
537}
538
539#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
540pub enum TextOverflow {
541    Clip,
542    Ellipsis,
543    Fade,
544    #[default]
545    Visible,
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
549pub enum TextDirection {
550    #[default]
551    Auto,
552    Ltr,
553    Rtl,
554}
555
556#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
557pub enum TextWidthBasis {
558    #[default]
559    Parent,
560    LongestLine,
561}
562
563#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
564pub enum MouseCursor {
565    #[default]
566    Basic,
567    Pointer,
568    Text,
569}
570
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
572pub struct TextHeightBehavior {
573    pub apply_height_to_first_ascent: bool,
574    pub apply_height_to_last_descent: bool,
575}
576
577impl Default for TextHeightBehavior {
578    fn default() -> Self {
579        Self {
580            apply_height_to_first_ascent: true,
581            apply_height_to_last_descent: true,
582        }
583    }
584}
585
586#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
587pub struct TextParagraphStyle {
588    pub text_align: TextAlign,
589    pub max_lines: Option<usize>,
590    pub overflow: TextOverflow,
591    #[serde(default)]
592    pub text_direction: TextDirection,
593    #[serde(default)]
594    pub text_width_basis: TextWidthBasis,
595    #[serde(default)]
596    pub strut_line_height: Option<LayoutUnit>,
597    #[serde(default)]
598    pub text_height_behavior: TextHeightBehavior,
599}
600
601impl PartialEq for TextParagraphStyle {
602    fn eq(&self, other: &Self) -> bool {
603        self.text_align == other.text_align
604            && self.max_lines == other.max_lines
605            && self.overflow == other.overflow
606            && self.text_direction == other.text_direction
607            && self.text_width_basis == other.text_width_basis
608            && self.strut_line_height.map(f32::to_bits) == other.strut_line_height.map(f32::to_bits)
609            && self.text_height_behavior == other.text_height_behavior
610    }
611}
612
613impl Eq for TextParagraphStyle {}
614
615impl std::hash::Hash for TextParagraphStyle {
616    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
617        self.text_align.hash(state);
618        self.max_lines.hash(state);
619        self.overflow.hash(state);
620        self.text_direction.hash(state);
621        self.text_width_basis.hash(state);
622        self.strut_line_height.map(f32::to_bits).hash(state);
623        self.text_height_behavior.hash(state);
624    }
625}
626
627const TEXT_PARAGRAPH_ALIGN_BITS: u32 = 0b111;
628const TEXT_PARAGRAPH_OVERFLOW_BITS: u32 = 0b111 << 3;
629const TEXT_PARAGRAPH_MAX_LINES_SHIFT: u32 = 6;
630const TEXT_PARAGRAPH_SENTINEL: u32 = 1;
631pub(super) const TEXT_PARAGRAPH_MAX_ENCODED_LINES: usize =
632    ((1 << 24) - 1) >> TEXT_PARAGRAPH_MAX_LINES_SHIFT;
633
634const fn text_align_code(align: TextAlign) -> u32 {
635    match align {
636        TextAlign::Start => 0,
637        TextAlign::Left => 1,
638        TextAlign::Center => 2,
639        TextAlign::Right => 3,
640        TextAlign::End => 4,
641        TextAlign::Justify => 5,
642    }
643}
644
645const fn text_overflow_code(overflow: TextOverflow) -> u32 {
646    match overflow {
647        TextOverflow::Visible => 0,
648        TextOverflow::Clip => 1,
649        TextOverflow::Ellipsis => 2,
650        TextOverflow::Fade => 3,
651    }
652}
653
654const fn decode_text_align(code: u32) -> TextAlign {
655    match code {
656        1 => TextAlign::Left,
657        2 => TextAlign::Center,
658        3 => TextAlign::Right,
659        4 => TextAlign::End,
660        5 => TextAlign::Justify,
661        _ => TextAlign::Start,
662    }
663}
664
665const fn decode_text_overflow(code: u32) -> TextOverflow {
666    match code {
667        1 => TextOverflow::Clip,
668        2 => TextOverflow::Ellipsis,
669        3 => TextOverflow::Fade,
670        _ => TextOverflow::Visible,
671    }
672}
673
674pub fn encode_text_paragraph_style(style: TextParagraphStyle) -> Option<LayoutUnit> {
675    if style == TextParagraphStyle::default() {
676        return None;
677    }
678    if style.text_direction != TextDirection::Auto
679        || style.text_width_basis != TextWidthBasis::Parent
680        || style.strut_line_height.is_some()
681        || style.text_height_behavior != TextHeightBehavior::default()
682    {
683        return None;
684    }
685
686    let max_lines = style
687        .max_lines
688        .unwrap_or(0)
689        .min(TEXT_PARAGRAPH_MAX_ENCODED_LINES) as u32;
690    let encoded = TEXT_PARAGRAPH_SENTINEL
691        + text_align_code(style.text_align)
692        + (text_overflow_code(style.overflow) << 3)
693        + (max_lines << TEXT_PARAGRAPH_MAX_LINES_SHIFT);
694
695    Some(-(encoded as LayoutUnit))
696}
697
698pub fn decode_text_paragraph_style(
699    encoded_width: Option<LayoutUnit>,
700) -> Option<TextParagraphStyle> {
701    let encoded_width = encoded_width?;
702    if !encoded_width.is_finite() || encoded_width >= 0.0 {
703        return None;
704    }
705
706    let raw = (-encoded_width).round();
707    if raw < TEXT_PARAGRAPH_SENTINEL as f32 {
708        return None;
709    }
710
711    let bits = raw as u32 - TEXT_PARAGRAPH_SENTINEL;
712    let text_align = decode_text_align(bits & TEXT_PARAGRAPH_ALIGN_BITS);
713    let overflow = decode_text_overflow((bits & TEXT_PARAGRAPH_OVERFLOW_BITS) >> 3);
714    let max_lines = match bits >> TEXT_PARAGRAPH_MAX_LINES_SHIFT {
715        0 => None,
716        lines => Some(lines as usize),
717    };
718
719    Some(TextParagraphStyle {
720        text_align,
721        max_lines,
722        overflow,
723        text_direction: TextDirection::Auto,
724        text_width_basis: TextWidthBasis::Parent,
725        strut_line_height: None,
726        text_height_behavior: TextHeightBehavior::default(),
727    })
728}
729
730#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
731pub enum FlexDirection {
732    Row,
733    Column,
734}
735
736impl Default for FlexDirection {
737    fn default() -> Self {
738        FlexDirection::Row
739    }
740}
741
742#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
743pub enum EmbedKind {
744    Video,
745    Web,
746    Custom(Vec<u8>),
747}
748
749#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
750pub enum GridTrack {
751    /// Fixed track size in logical points.
752    Points(LayoutUnit),
753    /// Percentage of the available grid axis. `50.0` means 50%.
754    Percent(f32),
755    /// Fraction of remaining free space after fixed and intrinsic tracks.
756    Fr(f32),
757    /// Track sized by the largest participating item's intrinsic size.
758    Auto,
759    /// Track sized by the participating items' minimum intrinsic size.
760    MinContent,
761    /// Track sized by the participating items' preferred intrinsic size.
762    MaxContent,
763    /// Track with independent minimum and maximum sizing functions.
764    MinMax(Box<GridTrack>, Box<GridTrack>),
765    /// Repeats an ordered track list a fixed number of times.
766    Repeat { count: u16, tracks: Vec<GridTrack> },
767    /// Repeats a track to fit available space, dropping empty trailing tracks.
768    AutoFit(Box<GridTrack>),
769    /// Repeats a track to fill available space, retaining empty tracks.
770    AutoFill(Box<GridTrack>),
771}
772
773/// The width source used to evaluate a responsive layout branch.
774#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
775pub enum ResponsiveQuery {
776    /// Compare breakpoints against the application viewport.
777    #[default]
778    Viewport,
779    /// Compare breakpoints against the constraints supplied by the parent.
780    Container,
781}
782
783/// An inclusive lower and exclusive upper width bound for a responsive branch.
784#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
785pub struct ResponsiveCondition {
786    /// Inclusive lower width bound.
787    pub min_width: Option<LayoutUnit>,
788    /// Exclusive upper width bound.
789    pub max_width: Option<LayoutUnit>,
790}
791
792impl ResponsiveCondition {
793    pub fn matches(self, width: LayoutUnit) -> bool {
794        self.min_width.is_none_or(|minimum| width >= minimum)
795            && self.max_width.is_none_or(|maximum| width < maximum)
796    }
797}
798
799impl std::hash::Hash for ResponsiveCondition {
800    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
801        self.min_width.map(f32::to_bits).hash(state);
802        self.max_width.map(f32::to_bits).hash(state);
803    }
804}
805
806impl GridTrack {
807    /// Creates a `minmax(min, max)` grid track.
808    ///
809    /// # Example
810    ///
811    /// ```rust
812    /// use fission_ir::op::GridTrack;
813    ///
814    /// let track = GridTrack::minmax(GridTrack::Points(180.0), GridTrack::Fr(1.0));
815    /// ```
816    pub fn minmax(min: GridTrack, max: GridTrack) -> Self {
817        Self::MinMax(Box::new(min), Box::new(max))
818    }
819
820    /// Repeats `tracks` `count` times.
821    pub fn repeat(count: u16, tracks: impl Into<Vec<GridTrack>>) -> Self {
822        Self::Repeat {
823            count,
824            tracks: tracks.into(),
825        }
826    }
827
828    /// Repeats `track` up to the available space and collapses empty tracks.
829    pub fn auto_fit(track: GridTrack) -> Self {
830        Self::AutoFit(Box::new(track))
831    }
832
833    /// Repeats `track` up to the available space and keeps empty tracks.
834    pub fn auto_fill(track: GridTrack) -> Self {
835        Self::AutoFill(Box::new(track))
836    }
837}
838
839impl std::hash::Hash for GridTrack {
840    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
841        match self {
842            Self::Points(u) => {
843                0.hash(state);
844                u.to_bits().hash(state);
845            }
846            Self::Percent(f) => {
847                1.hash(state);
848                f.to_bits().hash(state);
849            }
850            Self::Fr(f) => {
851                2.hash(state);
852                f.to_bits().hash(state);
853            }
854            Self::Auto => {
855                3.hash(state);
856            }
857            Self::MinContent => {
858                4.hash(state);
859            }
860            Self::MaxContent => {
861                5.hash(state);
862            }
863            Self::MinMax(min, max) => {
864                6.hash(state);
865                min.hash(state);
866                max.hash(state);
867            }
868            Self::Repeat { count, tracks } => {
869                7.hash(state);
870                count.hash(state);
871                tracks.hash(state);
872            }
873            Self::AutoFit(track) => {
874                8.hash(state);
875                track.hash(state);
876            }
877            Self::AutoFill(track) => {
878                9.hash(state);
879                track.hash(state);
880            }
881        }
882    }
883}
884
885#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
886pub enum GridPlacement {
887    /// Let the grid auto-placement algorithm choose the line.
888    Auto,
889    /// A one-based grid line number. Negative values count back from the end.
890    Line(i16),
891    /// Span this many tracks from the resolved start line.
892    Span(u16),
893}
894
895impl Default for GridPlacement {
896    fn default() -> Self {
897        Self::Auto
898    }
899}
900
901#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
902pub enum FlexWrap {
903    NoWrap,
904    Wrap,
905    WrapReverse,
906}
907
908impl Default for FlexWrap {
909    fn default() -> Self {
910        FlexWrap::NoWrap
911    }
912}
913
914#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
915pub enum AlignItems {
916    Start,
917    End,
918    Center,
919    Stretch,
920    Baseline,
921}
922
923impl Default for AlignItems {
924    fn default() -> Self {
925        AlignItems::Stretch
926    }
927}
928
929#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
930pub enum JustifyContent {
931    Start,
932    End,
933    Center,
934    SpaceBetween,
935    SpaceAround,
936    SpaceEvenly,
937}
938
939impl Default for JustifyContent {
940    fn default() -> Self {
941        JustifyContent::Start
942    }
943}
944
945#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
946pub enum LayoutOp {
947    Box {
948        width: Option<LayoutUnit>,
949        height: Option<LayoutUnit>,
950        min_width: Option<LayoutUnit>,
951        max_width: Option<LayoutUnit>,
952        min_height: Option<LayoutUnit>,
953        max_height: Option<LayoutUnit>,
954        padding: [LayoutUnit; 4],
955        flex_grow: LayoutUnit,
956        flex_shrink: LayoutUnit,
957        aspect_ratio: Option<f32>,
958    },
959    /// A common box using declarative length expressions.
960    StyledBox {
961        style: BoxStyle,
962        flex_grow: LayoutUnit,
963        flex_shrink: LayoutUnit,
964    },
965    Flex {
966        direction: FlexDirection,
967        wrap: FlexWrap,
968        flex_grow: LayoutUnit,
969        flex_shrink: LayoutUnit,
970        padding: [LayoutUnit; 4],
971        gap: Option<LayoutUnit>,
972        align_items: AlignItems,
973        justify_content: JustifyContent,
974    },
975    Grid {
976        columns: Vec<GridTrack>,
977        rows: Vec<GridTrack>,
978        column_gap: Option<LayoutUnit>,
979        row_gap: Option<LayoutUnit>,
980        padding: [LayoutUnit; 4],
981    },
982    GridItem {
983        row_start: GridPlacement,
984        row_end: GridPlacement,
985        col_start: GridPlacement,
986        col_end: GridPlacement,
987    },
988    /// Selects one case child or the final fallback child from local constraints.
989    Responsive {
990        query: ResponsiveQuery,
991        cases: Vec<ResponsiveCondition>,
992    },
993    Scroll {
994        direction: FlexDirection,
995        show_scrollbar: bool,
996        width: Option<LayoutUnit>,
997        height: Option<LayoutUnit>,
998        min_width: Option<LayoutUnit>,
999        max_width: Option<LayoutUnit>,
1000        min_height: Option<LayoutUnit>,
1001        max_height: Option<LayoutUnit>,
1002        padding: [LayoutUnit; 4],
1003        flex_grow: LayoutUnit,
1004        flex_shrink: LayoutUnit,
1005    },
1006    Embed {
1007        kind: EmbedKind,
1008        widget_id: WidgetId,
1009        width: Option<LayoutUnit>,
1010        height: Option<LayoutUnit>,
1011    },
1012    AbsoluteFill,
1013    Positioned {
1014        left: Option<LayoutUnit>,
1015        top: Option<LayoutUnit>,
1016        right: Option<LayoutUnit>,
1017        bottom: Option<LayoutUnit>,
1018        width: Option<LayoutUnit>,
1019        height: Option<LayoutUnit>,
1020    },
1021    /// Absolutely positions a child using typed lengths resolved by layout.
1022    PositionedLengths {
1023        left: Option<Length>,
1024        top: Option<Length>,
1025        right: Option<Length>,
1026        bottom: Option<Length>,
1027        width: Option<Length>,
1028        height: Option<Length>,
1029    },
1030    ZStack,
1031    Align,
1032    Flyout {
1033        anchor: WidgetId,
1034        content: WidgetId,
1035    },
1036    /// Lays out five overlay children around an external anchor.
1037    ///
1038    /// Children are ordered as top, bottom, left, right, and focus ring. The
1039    /// four surrounding regions leave the padded anchor rectangle uncovered.
1040    Spotlight {
1041        anchor: WidgetId,
1042        padding: LayoutUnit,
1043    },
1044    Transform {
1045        transform: [f32; 16],
1046    },
1047    InteractiveViewport {
1048        initial_transform: ViewportTransform,
1049        controlled_transform: Option<ViewportTransform>,
1050        pan_axis: ViewportPanAxis,
1051        boundary: ViewportBoundary,
1052        clip: ViewportClip,
1053        zoom_policy: ViewportZoomPolicy,
1054        min_scale: f32,
1055        max_scale: f32,
1056        friction: f32,
1057        on_interaction_start: Option<ActionEntry>,
1058        on_interaction_update: Option<ActionEntry>,
1059        on_interaction_end: Option<ActionEntry>,
1060    },
1061    Clip {
1062        path: Option<String>,
1063    },
1064}
1065
1066impl std::hash::Hash for LayoutOp {
1067    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1068        let hash_unit = |u: LayoutUnit, h: &mut H| u.to_bits().hash(h);
1069        let hash_opt_unit = |u: Option<LayoutUnit>, h: &mut H| u.map(|v| v.to_bits()).hash(h);
1070        let hash_units = |us: [LayoutUnit; 4], h: &mut H| {
1071            for u in us {
1072                u.to_bits().hash(h);
1073            }
1074        };
1075
1076        match self {
1077            Self::Box {
1078                width,
1079                height,
1080                min_width,
1081                max_width,
1082                min_height,
1083                max_height,
1084                padding,
1085                flex_grow,
1086                flex_shrink,
1087                aspect_ratio,
1088            } => {
1089                0.hash(state);
1090                hash_opt_unit(*width, state);
1091                hash_opt_unit(*height, state);
1092                hash_opt_unit(*min_width, state);
1093                hash_opt_unit(*max_width, state);
1094                hash_opt_unit(*min_height, state);
1095                hash_opt_unit(*max_height, state);
1096                hash_units(*padding, state);
1097                hash_unit(*flex_grow, state);
1098                hash_unit(*flex_shrink, state);
1099                aspect_ratio.map(|f| f.to_bits()).hash(state);
1100            }
1101            Self::StyledBox {
1102                style,
1103                flex_grow,
1104                flex_shrink,
1105            } => {
1106                13.hash(state);
1107                style.hash(state);
1108                hash_unit(*flex_grow, state);
1109                hash_unit(*flex_shrink, state);
1110            }
1111            Self::Flex {
1112                direction,
1113                wrap,
1114                flex_grow,
1115                flex_shrink,
1116                padding,
1117                gap,
1118                align_items,
1119                justify_content,
1120            } => {
1121                1.hash(state);
1122                direction.hash(state);
1123                wrap.hash(state);
1124                hash_unit(*flex_grow, state);
1125                hash_unit(*flex_shrink, state);
1126                hash_units(*padding, state);
1127                hash_opt_unit(*gap, state);
1128                align_items.hash(state);
1129                justify_content.hash(state);
1130            }
1131            Self::Grid {
1132                columns,
1133                rows,
1134                column_gap,
1135                row_gap,
1136                padding,
1137            } => {
1138                2.hash(state);
1139                columns.hash(state);
1140                rows.hash(state);
1141                hash_opt_unit(*column_gap, state);
1142                hash_opt_unit(*row_gap, state);
1143                hash_units(*padding, state);
1144            }
1145            Self::GridItem {
1146                row_start,
1147                row_end,
1148                col_start,
1149                col_end,
1150            } => {
1151                3.hash(state);
1152                row_start.hash(state);
1153                row_end.hash(state);
1154                col_start.hash(state);
1155                col_end.hash(state);
1156            }
1157            Self::Responsive { query, cases } => {
1158                14.hash(state);
1159                query.hash(state);
1160                cases.hash(state);
1161            }
1162            Self::Scroll {
1163                direction,
1164                show_scrollbar,
1165                width,
1166                height,
1167                min_width,
1168                max_width,
1169                min_height,
1170                max_height,
1171                padding,
1172                flex_grow,
1173                flex_shrink,
1174            } => {
1175                4.hash(state);
1176                direction.hash(state);
1177                show_scrollbar.hash(state);
1178                hash_opt_unit(*width, state);
1179                hash_opt_unit(*height, state);
1180                hash_opt_unit(*min_width, state);
1181                hash_opt_unit(*max_width, state);
1182                hash_opt_unit(*min_height, state);
1183                hash_opt_unit(*max_height, state);
1184                hash_units(*padding, state);
1185                hash_unit(*flex_grow, state);
1186                hash_unit(*flex_shrink, state);
1187            }
1188            Self::Embed {
1189                kind,
1190                widget_id,
1191                width,
1192                height,
1193            } => {
1194                5.hash(state);
1195                kind.hash(state);
1196                widget_id.hash(state);
1197                hash_opt_unit(*width, state);
1198                hash_opt_unit(*height, state);
1199            }
1200            Self::AbsoluteFill => {
1201                6.hash(state);
1202            }
1203            Self::Positioned {
1204                left,
1205                top,
1206                right,
1207                bottom,
1208                width,
1209                height,
1210            } => {
1211                7.hash(state);
1212                hash_opt_unit(*left, state);
1213                hash_opt_unit(*top, state);
1214                hash_opt_unit(*right, state);
1215                hash_opt_unit(*bottom, state);
1216                hash_opt_unit(*width, state);
1217                hash_opt_unit(*height, state);
1218            }
1219            Self::PositionedLengths {
1220                left,
1221                top,
1222                right,
1223                bottom,
1224                width,
1225                height,
1226            } => {
1227                15.hash(state);
1228                left.hash(state);
1229                top.hash(state);
1230                right.hash(state);
1231                bottom.hash(state);
1232                width.hash(state);
1233                height.hash(state);
1234            }
1235            Self::ZStack => {
1236                8.hash(state);
1237            }
1238            Self::Align => {
1239                9.hash(state);
1240            }
1241            Self::Flyout { anchor, content } => {
1242                10.hash(state);
1243                anchor.hash(state);
1244                content.hash(state);
1245            }
1246            Self::Transform { transform } => {
1247                11.hash(state);
1248                for v in transform {
1249                    v.to_bits().hash(state);
1250                }
1251            }
1252            Self::InteractiveViewport {
1253                initial_transform,
1254                controlled_transform,
1255                pan_axis,
1256                boundary,
1257                clip,
1258                zoom_policy,
1259                min_scale,
1260                max_scale,
1261                friction,
1262                on_interaction_start,
1263                on_interaction_update,
1264                on_interaction_end,
1265            } => {
1266                17.hash(state);
1267                initial_transform.hash(state);
1268                controlled_transform.hash(state);
1269                pan_axis.hash(state);
1270                boundary.hash(state);
1271                clip.hash(state);
1272                zoom_policy.hash(state);
1273                min_scale.to_bits().hash(state);
1274                max_scale.to_bits().hash(state);
1275                friction.to_bits().hash(state);
1276                on_interaction_start.hash(state);
1277                on_interaction_update.hash(state);
1278                on_interaction_end.hash(state);
1279            }
1280            Self::Clip { path } => {
1281                12.hash(state);
1282                path.hash(state);
1283            }
1284            Self::Spotlight { anchor, padding } => {
1285                16.hash(state);
1286                anchor.hash(state);
1287                hash_unit(*padding, state);
1288            }
1289        }
1290    }
1291}
1292
1293#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
1294pub struct Color {
1295    pub r: u8,
1296    pub g: u8,
1297    pub b: u8,
1298    pub a: u8,
1299}
1300
1301impl Color {
1302    pub const TRANSPARENT: Self = Self {
1303        r: 0,
1304        g: 0,
1305        b: 0,
1306        a: 0,
1307    };
1308    pub const BLACK: Self = Self {
1309        r: 0,
1310        g: 0,
1311        b: 0,
1312        a: 255,
1313    };
1314    pub const WHITE: Self = Self {
1315        r: 255,
1316        g: 255,
1317        b: 255,
1318        a: 255,
1319    };
1320    pub const RED: Self = Self {
1321        r: 255,
1322        g: 0,
1323        b: 0,
1324        a: 255,
1325    };
1326    pub const GREEN: Self = Self {
1327        r: 0,
1328        g: 255,
1329        b: 0,
1330        a: 255,
1331    };
1332    pub const BLUE: Self = Self {
1333        r: 0,
1334        g: 0,
1335        b: 255,
1336        a: 255,
1337    };
1338
1339    pub fn with_alpha(mut self, a: u8) -> Self {
1340        self.a = a;
1341        self
1342    }
1343}
1344
1345#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1346pub enum Fill {
1347    Solid(Color),
1348    /// A gradient whose start and end points are normalized to the painted
1349    /// bounds, where `(0.0, 0.0)` is the top-left and `(1.0, 1.0)` is the
1350    /// bottom-right.
1351    LinearGradient {
1352        start: (f32, f32),
1353        end: (f32, f32),
1354        stops: Vec<(f32, Color)>,
1355    },
1356    /// A gradient whose center and radius are normalized to the painted bounds.
1357    RadialGradient {
1358        center: (f32, f32),
1359        radius: f32,
1360        stops: Vec<(f32, Color)>,
1361    },
1362}
1363
1364impl std::hash::Hash for Fill {
1365    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1366        match self {
1367            Self::Solid(c) => {
1368                0.hash(state);
1369                c.hash(state);
1370            }
1371            Self::LinearGradient { start, end, stops } => {
1372                1.hash(state);
1373                start.0.to_bits().hash(state);
1374                start.1.to_bits().hash(state);
1375                end.0.to_bits().hash(state);
1376                end.1.to_bits().hash(state);
1377                for (off, c) in stops {
1378                    off.to_bits().hash(state);
1379                    c.hash(state);
1380                }
1381            }
1382            Self::RadialGradient {
1383                center,
1384                radius,
1385                stops,
1386            } => {
1387                2.hash(state);
1388                center.0.to_bits().hash(state);
1389                center.1.to_bits().hash(state);
1390                radius.to_bits().hash(state);
1391                for (off, c) in stops {
1392                    off.to_bits().hash(state);
1393                    c.hash(state);
1394                }
1395            }
1396        }
1397    }
1398}
1399
1400#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1401pub enum LineCap {
1402    Butt,
1403    Round,
1404    Square,
1405}
1406
1407#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1408pub enum LineJoin {
1409    Miter,
1410    Round,
1411    Bevel,
1412}
1413
1414#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1415pub struct Stroke {
1416    pub fill: Fill,
1417    pub width: LayoutUnit,
1418    pub dash_array: Option<Vec<f32>>,
1419    pub line_cap: LineCap,
1420    pub line_join: LineJoin,
1421}
1422
1423impl std::hash::Hash for Stroke {
1424    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1425        self.fill.hash(state);
1426        self.width.to_bits().hash(state);
1427        if let Some(da) = &self.dash_array {
1428            1.hash(state);
1429            for d in da {
1430                d.to_bits().hash(state);
1431            }
1432        } else {
1433            0.hash(state);
1434        }
1435        self.line_cap.hash(state);
1436        self.line_join.hash(state);
1437    }
1438}
1439
1440#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1441pub struct BoxShadow {
1442    pub color: Color,
1443    pub blur_radius: LayoutUnit,
1444    /// Positive values expand the shadow shape; negative values contract it.
1445    pub spread_radius: LayoutUnit,
1446    pub offset: (LayoutUnit, LayoutUnit),
1447    /// Draws the shadow inside the shape instead of behind it.
1448    pub inset: bool,
1449}
1450
1451impl std::hash::Hash for BoxShadow {
1452    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1453        self.color.hash(state);
1454        self.blur_radius.to_bits().hash(state);
1455        self.spread_radius.to_bits().hash(state);
1456        self.offset.0.to_bits().hash(state);
1457        self.offset.1.to_bits().hash(state);
1458        self.inset.hash(state);
1459    }
1460}
1461
1462#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
1463pub enum ImageFit {
1464    Contain,
1465    Cover,
1466    Fill,
1467    None,
1468}
1469
1470#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1471pub enum ImageAlignment {
1472    TopStart,
1473    TopCenter,
1474    TopEnd,
1475    CenterStart,
1476    #[default]
1477    Center,
1478    CenterEnd,
1479    BottomStart,
1480    BottomCenter,
1481    BottomEnd,
1482}
1483
1484#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
1485pub struct HttpHeader {
1486    pub name: String,
1487    pub value: String,
1488}
1489
1490#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1491pub enum ImageCachePolicy {
1492    #[default]
1493    Default,
1494    Reload,
1495    MemoryOnly,
1496    Disk,
1497    NoStore,
1498}
1499
1500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
1501pub enum ImageSource {
1502    Asset {
1503        path: String,
1504    },
1505    File {
1506        path: String,
1507    },
1508    Network {
1509        url: String,
1510        #[serde(default)]
1511        headers: Vec<HttpHeader>,
1512        #[serde(default)]
1513        cache_policy: ImageCachePolicy,
1514    },
1515    Memory {
1516        bytes: Vec<u8>,
1517        #[serde(default)]
1518        mime_type: Option<String>,
1519    },
1520    SvgText {
1521        content: String,
1522    },
1523}
1524
1525impl Default for ImageSource {
1526    fn default() -> Self {
1527        Self::Asset {
1528            path: String::new(),
1529        }
1530    }
1531}
1532
1533#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1534pub enum ImageLoadingBehavior {
1535    #[default]
1536    Empty,
1537    ThemePlaceholder,
1538    BlurHash(String),
1539}
1540
1541#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1542pub enum ImageErrorBehavior {
1543    #[default]
1544    Empty,
1545    ThemeError,
1546    AltText,
1547}
1548
1549#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
1550pub struct ImageRequest {
1551    pub source: ImageSource,
1552    #[serde(default)]
1553    pub cache_width: Option<u32>,
1554    #[serde(default)]
1555    pub cache_height: Option<u32>,
1556    #[serde(default)]
1557    pub semantic_label: Option<String>,
1558    #[serde(default)]
1559    pub loading: ImageLoadingBehavior,
1560    #[serde(default)]
1561    pub error: ImageErrorBehavior,
1562}
1563
1564impl ImageSource {
1565    pub fn stable_identity(&self) -> String {
1566        match self {
1567            Self::Asset { path } => format!("asset:{path}"),
1568            Self::File { path } => format!("file:{path}"),
1569            Self::Network {
1570                url,
1571                headers,
1572                cache_policy,
1573            } => {
1574                let mut identity = format!("network:{cache_policy:?}:{url}");
1575                for header in headers {
1576                    identity.push('|');
1577                    identity.push_str(&header.name.to_ascii_lowercase());
1578                    identity.push('=');
1579                    identity.push_str(&header.value);
1580                }
1581                identity
1582            }
1583            Self::Memory { bytes, mime_type } => {
1584                let digest = blake3::hash(bytes);
1585                format!("memory:{}:{digest}", mime_type.as_deref().unwrap_or(""))
1586            }
1587            Self::SvgText { content } => {
1588                let digest = blake3::hash(content.as_bytes());
1589                format!("svg:{digest}")
1590            }
1591        }
1592    }
1593
1594    pub fn local_path(&self) -> Option<&str> {
1595        match self {
1596            Self::Asset { path } | Self::File { path } => Some(path),
1597            _ => None,
1598        }
1599    }
1600
1601    pub fn network_url(&self) -> Option<&str> {
1602        match self {
1603            Self::Network { url, .. } => Some(url),
1604            _ => None,
1605        }
1606    }
1607}
1608
1609impl ImageRequest {
1610    pub fn stable_cache_key(&self) -> String {
1611        let mut hasher = blake3::Hasher::new();
1612        hasher.update(self.source.stable_identity().as_bytes());
1613        hasher.update(&self.cache_width.unwrap_or_default().to_le_bytes());
1614        hasher.update(&self.cache_height.unwrap_or_default().to_le_bytes());
1615        hasher.finalize().to_hex().to_string()
1616    }
1617}
1618
1619#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1620pub struct TextStyle {
1621    pub font_size: LayoutUnit,
1622    pub color: Color,
1623    pub underline: bool,
1624    #[serde(default)]
1625    pub font_family: Option<String>,
1626    #[serde(default)]
1627    pub locale: Option<String>,
1628    #[serde(default = "text_weight_default")]
1629    pub font_weight: u16,
1630    #[serde(default)]
1631    pub font_style: FontStyle,
1632    #[serde(default)]
1633    pub line_height: Option<LayoutUnit>,
1634    #[serde(default)]
1635    pub letter_spacing: LayoutUnit,
1636    /// Optional background highlight color for this run (find matches, error squiggles, etc.).
1637    pub background_color: Option<Color>,
1638}
1639
1640impl std::hash::Hash for TextStyle {
1641    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1642        self.font_size.to_bits().hash(state);
1643        self.color.hash(state);
1644        self.underline.hash(state);
1645        self.font_family.hash(state);
1646        self.locale.hash(state);
1647        self.font_weight.hash(state);
1648        self.font_style.hash(state);
1649        self.line_height.map(f32::to_bits).hash(state);
1650        self.letter_spacing.to_bits().hash(state);
1651        self.background_color.hash(state);
1652    }
1653}
1654
1655#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1656pub enum FontStyle {
1657    #[default]
1658    Normal,
1659    Italic,
1660}
1661
1662const fn text_weight_default() -> u16 {
1663    400
1664}
1665
1666#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
1667pub struct TextRun {
1668    pub text: String,
1669    pub style: TextStyle,
1670}
1671
1672#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
1673pub struct RichTextAnnotation {
1674    pub range: std::ops::Range<usize>,
1675    #[serde(default)]
1676    pub semantics_label: Option<String>,
1677    #[serde(default)]
1678    pub semantics_identifier: Option<String>,
1679    #[serde(default)]
1680    pub spell_out: Option<bool>,
1681    #[serde(default)]
1682    pub mouse_cursor: Option<MouseCursor>,
1683    #[serde(default)]
1684    pub actions: Vec<ActionEntry>,
1685}
1686
1687pub const INLINE_WIDGET_MARKER_PREFIX: &str = "__fission_inline_widget__:";
1688
1689#[derive(Debug, Clone, Copy, PartialEq)]
1690pub struct InlineWidgetMarker {
1691    pub id: u64,
1692    pub width: LayoutUnit,
1693    pub height: LayoutUnit,
1694}
1695
1696pub fn encode_inline_widget_marker(id: u64, width: LayoutUnit, height: LayoutUnit) -> String {
1697    format!("{INLINE_WIDGET_MARKER_PREFIX}{id}:{width}:{height}")
1698}
1699
1700pub fn decode_inline_widget_marker(family: Option<&str>) -> Option<InlineWidgetMarker> {
1701    let family = family?;
1702    let encoded = family.strip_prefix(INLINE_WIDGET_MARKER_PREFIX)?;
1703    let mut parts = encoded.split(':');
1704    let id = parts.next()?.parse().ok()?;
1705    let width = parts.next()?.parse().ok()?;
1706    let height = parts.next()?.parse().ok()?;
1707    if parts.next().is_some() {
1708        return None;
1709    }
1710    Some(InlineWidgetMarker { id, width, height })
1711}
1712
1713const fn text_wrap_default() -> bool {
1714    true
1715}
1716
1717/// A filter applied to content already painted behind a widget.
1718#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1719pub enum BackdropFilter {
1720    /// Applies a Gaussian blur using the supplied standard deviation.
1721    Blur(LayoutUnit),
1722}
1723
1724impl std::hash::Hash for BackdropFilter {
1725    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1726        match self {
1727            Self::Blur(sigma) => {
1728                0_u8.hash(state);
1729                sigma.to_bits().hash(state);
1730            }
1731        }
1732    }
1733}
1734
1735#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1736pub enum PaintOp {
1737    BackdropFilter {
1738        filter: BackdropFilter,
1739        corner_radius: LayoutUnit,
1740    },
1741    DrawRect {
1742        fill: Option<Fill>,
1743        stroke: Option<Stroke>,
1744        corner_radius: LayoutUnit,
1745        shadow: Option<BoxShadow>,
1746    },
1747    DrawText {
1748        text: String,
1749        size: LayoutUnit,
1750        color: Color,
1751        underline: bool,
1752        #[serde(default = "text_wrap_default")]
1753        wrap: bool,
1754        caret_index: Option<usize>,
1755        #[serde(default)]
1756        caret_color: Option<Color>,
1757        #[serde(default)]
1758        caret_width: Option<LayoutUnit>,
1759        #[serde(default)]
1760        caret_height: Option<LayoutUnit>,
1761        #[serde(default)]
1762        caret_radius: Option<LayoutUnit>,
1763        #[serde(default)]
1764        paragraph_style: Option<TextParagraphStyle>,
1765    },
1766    DrawRichText {
1767        runs: Vec<TextRun>,
1768        #[serde(default = "text_wrap_default")]
1769        wrap: bool,
1770        caret_index: Option<usize>,
1771        #[serde(default)]
1772        caret_color: Option<Color>,
1773        #[serde(default)]
1774        caret_width: Option<LayoutUnit>,
1775        #[serde(default)]
1776        caret_height: Option<LayoutUnit>,
1777        #[serde(default)]
1778        caret_radius: Option<LayoutUnit>,
1779        #[serde(default)]
1780        paragraph_style: Option<TextParagraphStyle>,
1781    },
1782    DrawImage {
1783        request: ImageRequest,
1784        fit: ImageFit,
1785        alignment: ImageAlignment,
1786    },
1787    DrawPath {
1788        path: String,
1789        fill: Option<Fill>,
1790        stroke: Option<Stroke>,
1791    },
1792    DrawSvg {
1793        content: String,
1794        fill: Option<Fill>,
1795        stroke: Option<Stroke>,
1796    },
1797}
1798
1799impl std::hash::Hash for PaintOp {
1800    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1801        match self {
1802            Self::BackdropFilter {
1803                filter,
1804                corner_radius,
1805            } => {
1806                0_u8.hash(state);
1807                filter.hash(state);
1808                corner_radius.to_bits().hash(state);
1809            }
1810            Self::DrawRect {
1811                fill,
1812                stroke,
1813                corner_radius,
1814                shadow,
1815            } => {
1816                1_u8.hash(state);
1817                fill.hash(state);
1818                stroke.hash(state);
1819                corner_radius.to_bits().hash(state);
1820                shadow.hash(state);
1821            }
1822            Self::DrawText {
1823                text,
1824                size,
1825                color,
1826                underline,
1827                wrap,
1828                caret_index,
1829                caret_color,
1830                caret_width,
1831                caret_height,
1832                caret_radius,
1833                paragraph_style,
1834            } => {
1835                2_u8.hash(state);
1836                text.hash(state);
1837                size.to_bits().hash(state);
1838                color.hash(state);
1839                underline.hash(state);
1840                wrap.hash(state);
1841                caret_index.hash(state);
1842                caret_color.hash(state);
1843                caret_width.map(|w| w.to_bits()).hash(state);
1844                caret_height.map(|h| h.to_bits()).hash(state);
1845                caret_radius.map(|r| r.to_bits()).hash(state);
1846                paragraph_style.hash(state);
1847            }
1848            Self::DrawRichText {
1849                runs,
1850                wrap,
1851                caret_index,
1852                caret_color,
1853                caret_width,
1854                caret_height,
1855                caret_radius,
1856                paragraph_style,
1857            } => {
1858                3_u8.hash(state);
1859                runs.hash(state);
1860                wrap.hash(state);
1861                caret_index.hash(state);
1862                caret_color.hash(state);
1863                caret_width.map(|w| w.to_bits()).hash(state);
1864                caret_height.map(|h| h.to_bits()).hash(state);
1865                caret_radius.map(|r| r.to_bits()).hash(state);
1866                paragraph_style.hash(state);
1867            }
1868            Self::DrawImage {
1869                request,
1870                fit,
1871                alignment,
1872            } => {
1873                4_u8.hash(state);
1874                request.hash(state);
1875                fit.hash(state);
1876                alignment.hash(state);
1877            }
1878            Self::DrawPath { path, fill, stroke } => {
1879                5_u8.hash(state);
1880                path.hash(state);
1881                fill.hash(state);
1882                stroke.hash(state);
1883            }
1884            Self::DrawSvg {
1885                content,
1886                fill,
1887                stroke,
1888            } => {
1889                6_u8.hash(state);
1890                content.hash(state);
1891                fill.hash(state);
1892                stroke.hash(state);
1893            }
1894        }
1895    }
1896}