Skip to main content

fission_ir/
op.rs

1//! Operations that IR nodes can perform.
2//!
3//! Every [`CoreNode`](crate::CoreNode) carries exactly one [`Op`]. The four
4//! categories cover everything a node can *do*:
5//!
6//! | Category | Type | Purpose |
7//! |----------|------|---------|
8//! | Structure | [`StructuralOp`] | Grouping nodes without visual effect. |
9//! | Layout | [`LayoutOp`] | Sizing and positioning children (Box, Flex, Grid, ...). |
10//! | Paint | [`PaintOp`] | Drawing rectangles, text, images, paths, and SVGs. |
11//! | Semantics | [`Semantics`] | Accessibility roles, labels, and action bindings. |
12//!
13//! Supporting types for colors, fills, strokes, text styles, flex parameters, and
14//! grid tracks are also defined here.
15
16use super::semantics::Semantics;
17use super::widget_id::WidgetNodeId;
18use crate::NodeId;
19use serde::{Deserialize, Serialize};
20
21/// The operation a node performs.
22///
23/// `Op` is the heart of the IR: it says what a [`CoreNode`](crate::CoreNode) *does*.
24/// There are exactly four categories, each wrapping a more specific enum or struct.
25///
26/// # Example
27///
28/// ```rust
29/// use fission_ir::{Op, LayoutOp};
30///
31/// let op = Op::Layout(LayoutOp::Box {
32///     width: Some(100.0), height: Some(50.0),
33///     min_width: None, max_width: None,
34///     min_height: None, max_height: None,
35///     padding: [0.0; 4], flex_grow: 0.0, flex_shrink: 1.0,
36///     aspect_ratio: None,
37/// });
38/// ```
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub enum Op {
41    /// A grouping node with no visual or layout effect. See [`StructuralOp`].
42    Structural(StructuralOp),
43    /// A layout node that sizes and positions its children. See [`LayoutOp`].
44    Layout(LayoutOp),
45    /// A paint node that draws something on screen. See [`PaintOp`].
46    Paint(PaintOp),
47    /// A semantics node that declares accessibility and interaction metadata.
48    /// See [`Semantics`](crate::Semantics).
49    Semantics(Semantics),
50}
51
52impl std::hash::Hash for Op {
53    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
54        match self {
55            Self::Structural(s) => { 0.hash(state); s.hash(state); }
56            Self::Layout(l) => { 1.hash(state); l.hash(state); }
57            Self::Paint(p) => { 2.hash(state); p.hash(state); }
58            Self::Semantics(s) => { 3.hash(state); s.hash(state); }
59        }
60    }
61}
62
63/// A structural operation that groups child nodes without any visual or layout effect.
64///
65/// Structural nodes exist so that the widget compiler can preserve logical grouping
66/// boundaries in the IR. They are transparent to the layout engine and the renderer.
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
68pub enum StructuralOp {
69    /// Groups children under a single parent. The `stable_hash` is a content hash
70    /// of the group's children, used for efficient diffing.
71    Group {
72        /// Content hash of the grouped subtree. Two groups with the same children
73        /// produce the same hash.
74        stable_hash: u64,
75    },
76}
77
78/// The scalar type used for all layout measurements (widths, heights, padding, etc.).
79///
80/// Currently `f32`. Using a type alias makes it easy to change precision globally.
81pub type LayoutUnit = f32;
82
83/// The primary axis direction for a flex or scroll container.
84///
85/// Determines whether children are laid out horizontally or vertically.
86///
87/// Defaults to [`Row`](FlexDirection::Row).
88#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
89pub enum FlexDirection {
90    /// Children are laid out left-to-right along the horizontal axis.
91    Row,
92    /// Children are laid out top-to-bottom along the vertical axis.
93    Column,
94}
95
96impl Default for FlexDirection {
97    fn default() -> Self {
98        FlexDirection::Row
99    }
100}
101
102/// The kind of platform-native surface embedded in the UI.
103///
104/// Used by [`LayoutOp::Embed`] to tell the platform layer what type of native
105/// view to create and manage.
106#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
107pub enum EmbedKind {
108    /// A video playback surface.
109    Video,
110    /// A web browser view (e.g., WKWebView, WebView2).
111    Web,
112    /// A custom platform-native view not covered by the other variants.
113    Custom,
114}
115
116/// A track sizing function for CSS Grid-style columns or rows.
117///
118/// Grid tracks define how available space is divided among columns and rows in a
119/// [`LayoutOp::Grid`]. They work like the CSS `grid-template-columns` /
120/// `grid-template-rows` values.
121///
122/// # Example
123///
124/// A three-column grid: 200px fixed, 1fr flexible, auto-sized:
125///
126/// ```rust
127/// use fission_ir::op::GridTrack;
128/// let columns = vec![GridTrack::Points(200.0), GridTrack::Fr(1.0), GridTrack::Auto];
129/// ```
130#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
131pub enum GridTrack {
132    /// A fixed size in logical pixels.
133    Points(LayoutUnit),
134    /// A percentage of the grid container's available space (0.0 to 100.0).
135    Percent(f32),
136    /// A fractional unit. Remaining space after fixed and percent tracks is divided
137    /// proportionally among `Fr` tracks.
138    Fr(f32),
139    /// Size to fit the content, with no minimum or maximum constraint.
140    Auto,
141    /// Size to the minimum content width/height (the narrowest the content can be
142    /// without overflow).
143    MinContent,
144    /// Size to the maximum content width/height (the widest the content wants to be).
145    MaxContent,
146}
147
148impl std::hash::Hash for GridTrack {
149    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
150        match self {
151            Self::Points(u) => { 0.hash(state); u.to_bits().hash(state); }
152            Self::Percent(f) => { 1.hash(state); f.to_bits().hash(state); }
153            Self::Fr(f) => { 2.hash(state); f.to_bits().hash(state); }
154            Self::Auto => { 3.hash(state); }
155            Self::MinContent => { 4.hash(state); }
156            Self::MaxContent => { 5.hash(state); }
157        }
158    }
159}
160
161/// Where a grid item is placed within its grid container.
162///
163/// Used by [`LayoutOp::GridItem`] to specify which row/column a child occupies.
164/// Works like the CSS `grid-row-start` / `grid-column-start` properties.
165///
166/// Defaults to [`Auto`](GridPlacement::Auto).
167#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
168pub enum GridPlacement {
169    /// Let the grid auto-placement algorithm choose the position.
170    Auto,
171    /// Place at a specific grid line (1-indexed, matching CSS convention).
172    Line(i16),
173    /// Span across the given number of tracks from the start position.
174    Span(u16),
175}
176
177impl Default for GridPlacement {
178    fn default() -> Self { Self::Auto }
179}
180
181/// Whether a flex container wraps children onto multiple lines.
182///
183/// Equivalent to the CSS `flex-wrap` property.
184///
185/// Defaults to [`NoWrap`](FlexWrap::NoWrap).
186#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
187pub enum FlexWrap {
188    /// All children stay on a single line, potentially overflowing.
189    NoWrap,
190    /// Children wrap onto additional lines in the normal direction.
191    Wrap,
192    /// Children wrap onto additional lines in the reverse direction.
193    WrapReverse,
194}
195
196impl Default for FlexWrap {
197    fn default() -> Self {
198        FlexWrap::NoWrap
199    }
200}
201
202/// How children are aligned on the cross axis of a flex container.
203///
204/// Equivalent to the CSS `align-items` property.
205///
206/// Defaults to [`Stretch`](AlignItems::Stretch).
207#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
208pub enum AlignItems {
209    /// Align children to the start of the cross axis.
210    Start,
211    /// Align children to the end of the cross axis.
212    End,
213    /// Center children on the cross axis.
214    Center,
215    /// Stretch children to fill the cross axis. This is the default.
216    Stretch,
217    /// Align children so their text baselines line up.
218    Baseline,
219}
220
221impl Default for AlignItems {
222    fn default() -> Self {
223        AlignItems::Stretch
224    }
225}
226
227/// How children are distributed along the main axis of a flex container.
228///
229/// Equivalent to the CSS `justify-content` property.
230///
231/// Defaults to [`Start`](JustifyContent::Start).
232#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
233pub enum JustifyContent {
234    /// Pack children toward the start of the main axis.
235    Start,
236    /// Pack children toward the end of the main axis.
237    End,
238    /// Center children along the main axis.
239    Center,
240    /// Distribute children so the first is at the start, the last at the end,
241    /// and equal space between each pair.
242    SpaceBetween,
243    /// Distribute children with equal space around each child (half-size spaces
244    /// on the edges).
245    SpaceAround,
246    /// Distribute children with exactly equal space between and around them.
247    SpaceEvenly,
248}
249
250impl Default for JustifyContent {
251    fn default() -> Self {
252        JustifyContent::Start
253    }
254}
255
256/// A layout operation that sizes and positions a node and its children.
257///
258/// `LayoutOp` covers every layout model in Fission: constrained boxes, flexbox,
259/// CSS Grid, scroll containers, absolute positioning, z-stacking, flyout menus,
260/// transforms, and clipping. Each variant maps to a distinct layout algorithm in
261/// the [`fission_layout`] crate.
262///
263/// # Padding convention
264///
265/// All `padding` fields use `[left, right, top, bottom]` order.
266///
267/// # Flex participation
268///
269/// Variants that have `flex_grow` and `flex_shrink` fields participate in flex
270/// layout when placed inside a `Flex` parent. `flex_grow` controls how much extra
271/// space the node claims; `flex_shrink` controls how much it gives up when the
272/// container overflows.
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274pub enum LayoutOp {
275    /// A constrained box that sizes itself and stacks its children.
276    ///
277    /// This is the most common layout node. It applies optional fixed dimensions,
278    /// min/max constraints, padding, and aspect ratio. Children are stacked on top
279    /// of each other (like a single-child container).
280    ///
281    /// Use `Box` when you need a container with specific size constraints, padding,
282    /// or an aspect ratio, but do not need flex or grid distribution.
283    Box {
284        /// Fixed width in logical pixels, or `None` to size-to-content.
285        width: Option<LayoutUnit>,
286        /// Fixed height in logical pixels, or `None` to size-to-content.
287        height: Option<LayoutUnit>,
288        /// Minimum width. The node will never be narrower than this.
289        min_width: Option<LayoutUnit>,
290        /// Maximum width. The node will never be wider than this.
291        max_width: Option<LayoutUnit>,
292        /// Minimum height. The node will never be shorter than this.
293        min_height: Option<LayoutUnit>,
294        /// Maximum height. The node will never be taller than this.
295        max_height: Option<LayoutUnit>,
296        /// Inner padding: `[left, right, top, bottom]`.
297        padding: [LayoutUnit; 4],
298        /// How much extra space this node claims when inside a flex container.
299        /// `0.0` means it does not grow. Default: `0.0`.
300        flex_grow: LayoutUnit,
301        /// How much this node shrinks when a flex container overflows.
302        /// `1.0` means it shrinks proportionally. Default: `1.0`.
303        flex_shrink: LayoutUnit,
304        /// If set, the node maintains this width/height ratio. For example,
305        /// `Some(16.0 / 9.0)` gives a widescreen aspect ratio.
306        aspect_ratio: Option<f32>,
307    },
308    /// A flex container that distributes children along a main axis.
309    ///
310    /// Implements CSS Flexbox semantics: children are measured, flex-grow/shrink is
311    /// applied, and then children are positioned according to `justify_content` and
312    /// `align_items`.
313    Flex {
314        /// Whether children flow horizontally ([`Row`](FlexDirection::Row)) or
315        /// vertically ([`Column`](FlexDirection::Column)).
316        direction: FlexDirection,
317        /// Whether children wrap onto multiple lines. Default: [`NoWrap`](FlexWrap::NoWrap).
318        wrap: FlexWrap,
319        /// How much extra space this flex container claims from *its* parent flex.
320        flex_grow: LayoutUnit,
321        /// How much this flex container shrinks when its parent overflows.
322        flex_shrink: LayoutUnit,
323        /// Inner padding: `[left, right, top, bottom]`.
324        padding: [LayoutUnit; 4],
325        /// Space between children along the main axis. `None` means `0.0`.
326        gap: Option<LayoutUnit>,
327        /// Cross-axis alignment of children. Default: [`Stretch`](AlignItems::Stretch).
328        align_items: AlignItems,
329        /// Main-axis distribution of children. Default: [`Start`](JustifyContent::Start).
330        justify_content: JustifyContent,
331    },
332    /// A CSS Grid container that places children into a row/column matrix.
333    ///
334    /// Columns and rows are defined by [`GridTrack`] sizing functions. Children are
335    /// placed either automatically (in source order) or explicitly via
336    /// [`GridItem`](LayoutOp::GridItem).
337    Grid {
338        /// Column track definitions. If empty, a single auto-width column is used.
339        columns: Vec<GridTrack>,
340        /// Row track definitions. If empty, rows are created automatically as needed.
341        rows: Vec<GridTrack>,
342        /// Horizontal gap between columns in logical pixels.
343        column_gap: Option<LayoutUnit>,
344        /// Vertical gap between rows in logical pixels.
345        row_gap: Option<LayoutUnit>,
346        /// Inner padding: `[left, right, top, bottom]`.
347        padding: [LayoutUnit; 4],
348    },
349    /// A child of a [`Grid`](LayoutOp::Grid) that specifies its row/column placement.
350    ///
351    /// If a grid child does not use `GridItem`, the grid auto-placement algorithm
352    /// assigns it the next available cell.
353    GridItem {
354        /// Which row line this item starts at. Default: [`Auto`](GridPlacement::Auto).
355        row_start: GridPlacement,
356        /// Which row line this item ends at. Default: [`Auto`](GridPlacement::Auto).
357        row_end: GridPlacement,
358        /// Which column line this item starts at. Default: [`Auto`](GridPlacement::Auto).
359        col_start: GridPlacement,
360        /// Which column line this item ends at. Default: [`Auto`](GridPlacement::Auto).
361        col_end: GridPlacement,
362    },
363    /// A scrollable container.
364    ///
365    /// The scroll container clips its content and shifts it by a scroll offset
366    /// obtained from a [`ScrollDataSource`](fission_layout). The layout engine
367    /// gives the content infinite space along the scroll axis so it can measure
368    /// its natural size.
369    Scroll {
370        /// Scroll axis: horizontal ([`Row`](FlexDirection::Row)) or vertical
371        /// ([`Column`](FlexDirection::Column)).
372        direction: FlexDirection,
373        /// Whether to render a scrollbar indicator.
374        show_scrollbar: bool,
375        /// Fixed width, or `None` to size from constraints.
376        width: Option<LayoutUnit>,
377        /// Fixed height, or `None` to size from constraints.
378        height: Option<LayoutUnit>,
379        /// Minimum width constraint.
380        min_width: Option<LayoutUnit>,
381        /// Maximum width constraint.
382        max_width: Option<LayoutUnit>,
383        /// Minimum height constraint.
384        min_height: Option<LayoutUnit>,
385        /// Maximum height constraint.
386        max_height: Option<LayoutUnit>,
387        /// Inner padding: `[left, right, top, bottom]`.
388        padding: [LayoutUnit; 4],
389        /// Flex grow factor when inside a flex parent.
390        flex_grow: LayoutUnit,
391        /// Flex shrink factor when inside a flex parent.
392        flex_shrink: LayoutUnit,
393    },
394    /// A placeholder for a platform-native surface (video, web view, etc.).
395    ///
396    /// The layout engine allocates space for the embed; the platform layer is
397    /// responsible for creating and positioning the actual native view.
398    Embed {
399        /// What kind of native surface to create.
400        kind: EmbedKind,
401        /// The widget that owns this native surface.
402        widget_id: WidgetNodeId,
403        /// Fixed width, or `None` to use available space.
404        width: Option<LayoutUnit>,
405        /// Fixed height, or `None` to use available space.
406        height: Option<LayoutUnit>,
407    },
408    /// A child that fills its parent's entire bounds.
409    ///
410    /// Equivalent to `Positioned { left: 0, top: 0, right: 0, bottom: 0 }` but
411    /// expressed as a zero-field variant for clarity. Commonly used for overlays,
412    /// backgrounds, and hit-test areas.
413    AbsoluteFill,
414    /// A child positioned absolutely within its parent.
415    ///
416    /// At least one of `left`/`right` and one of `top`/`bottom` should be set.
417    /// If both `left` and `right` are set (and `width` is not), the width is
418    /// inferred from the parent's width minus both offsets.
419    Positioned {
420        /// Offset from the parent's left edge.
421        left: Option<LayoutUnit>,
422        /// Offset from the parent's top edge.
423        top: Option<LayoutUnit>,
424        /// Offset from the parent's right edge.
425        right: Option<LayoutUnit>,
426        /// Offset from the parent's bottom edge.
427        bottom: Option<LayoutUnit>,
428        /// Fixed width. If `None`, width is inferred from `left`/`right`.
429        width: Option<LayoutUnit>,
430        /// Fixed height. If `None`, height is inferred from `top`/`bottom`.
431        height: Option<LayoutUnit>,
432    },
433    /// A container that stacks all children on top of each other.
434    ///
435    /// Each child occupies the full size of the stack; later children paint on
436    /// top of earlier ones. The stack's own size is the union of its children.
437    ZStack,
438    /// A container that centers its single child within the available space.
439    Align,
440    /// An anchored popup container (dropdown menu, tooltip, etc.).
441    ///
442    /// The `content` node is positioned relative to the `anchor` node's screen
443    /// location, typically directly below it. The layout engine resolves anchor
444    /// positions after the main layout pass.
445    Flyout {
446        /// The node that the flyout is anchored to.
447        anchor: NodeId,
448        /// The node containing the flyout content.
449        content: NodeId,
450    },
451    /// Applies a 4x4 affine transform matrix to its child.
452    ///
453    /// The matrix is column-major, matching OpenGL/wgpu convention. The transform
454    /// does not affect layout; it is applied during painting.
455    Transform {
456        /// A 4x4 column-major transform matrix.
457        transform: [f32; 16],
458    },
459    /// Clips its child to a rectangular or path-defined region.
460    ///
461    /// If `path` is `None`, the clip is the node's layout rectangle. If `path` is
462    /// set, it is an SVG-style path string.
463    Clip {
464        /// An optional SVG path string. `None` means clip to the layout rect.
465        path: Option<String>,
466    },
467}
468
469impl std::hash::Hash for LayoutOp {
470    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
471        let hash_unit = |u: LayoutUnit, h: &mut H| u.to_bits().hash(h);
472        let hash_opt_unit = |u: Option<LayoutUnit>, h: &mut H| u.map(|v| v.to_bits()).hash(h);
473        let hash_units = |us: [LayoutUnit; 4], h: &mut H| { for u in us { u.to_bits().hash(h); } };
474
475        match self {
476            Self::Box { width, height, min_width, max_width, min_height, max_height, padding, flex_grow, flex_shrink, aspect_ratio } => {
477                0.hash(state); hash_opt_unit(*width, state); hash_opt_unit(*height, state);
478                hash_opt_unit(*min_width, state); hash_opt_unit(*max_width, state);
479                hash_opt_unit(*min_height, state); hash_opt_unit(*max_height, state);
480                hash_units(*padding, state); hash_unit(*flex_grow, state); hash_unit(*flex_shrink, state);
481                aspect_ratio.map(|f| f.to_bits()).hash(state);
482            }
483            Self::Flex { direction, wrap, flex_grow, flex_shrink, padding, gap, align_items, justify_content } => {
484                1.hash(state); direction.hash(state); wrap.hash(state);
485                hash_unit(*flex_grow, state); hash_unit(*flex_shrink, state);
486                hash_units(*padding, state); hash_opt_unit(*gap, state);
487                align_items.hash(state); justify_content.hash(state);
488            }
489            Self::Grid { columns, rows, column_gap, row_gap, padding } => {
490                2.hash(state); columns.hash(state); rows.hash(state);
491                hash_opt_unit(*column_gap, state); hash_opt_unit(*row_gap, state);
492                hash_units(*padding, state);
493            }
494            Self::GridItem { row_start, row_end, col_start, col_end } => {
495                3.hash(state); row_start.hash(state); row_end.hash(state); col_start.hash(state); col_end.hash(state);
496            }
497            Self::Scroll { direction, show_scrollbar, width, height, min_width, max_width, min_height, max_height, padding, flex_grow, flex_shrink } => {
498                4.hash(state); direction.hash(state); show_scrollbar.hash(state);
499                hash_opt_unit(*width, state); hash_opt_unit(*height, state);
500                hash_opt_unit(*min_width, state); hash_opt_unit(*max_width, state);
501                hash_opt_unit(*min_height, state); hash_opt_unit(*max_height, state);
502                hash_units(*padding, state); hash_unit(*flex_grow, state); hash_unit(*flex_shrink, state);
503            }
504            Self::Embed { kind, widget_id, width, height } => {
505                5.hash(state); kind.hash(state); widget_id.hash(state);
506                hash_opt_unit(*width, state); hash_opt_unit(*height, state);
507            }
508            Self::AbsoluteFill => { 6.hash(state); }
509            Self::Positioned { left, top, right, bottom, width, height } => {
510                7.hash(state); hash_opt_unit(*left, state); hash_opt_unit(*top, state);
511                hash_opt_unit(*right, state); hash_opt_unit(*bottom, state);
512                hash_opt_unit(*width, state); hash_opt_unit(*height, state);
513            }
514            Self::ZStack => { 8.hash(state); }
515            Self::Align => { 9.hash(state); }
516            Self::Flyout { anchor, content } => { 10.hash(state); anchor.hash(state); content.hash(state); }
517            Self::Transform { transform } => { 11.hash(state); for v in transform { v.to_bits().hash(state); } }
518            Self::Clip { path } => { 12.hash(state); path.hash(state); }
519        }
520    }
521}
522
523/// An RGBA color with 8-bit channels.
524///
525/// Colors are used throughout the IR for fills, strokes, text, and shadows.
526/// Several named constants are provided for common colors.
527///
528/// # Example
529///
530/// ```rust
531/// use fission_ir::op::Color;
532///
533/// let semi_transparent_red = Color::RED.with_alpha(128);
534/// assert_eq!(semi_transparent_red.a, 128);
535/// ```
536#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
537pub struct Color {
538    /// Red channel (0-255).
539    pub r: u8,
540    /// Green channel (0-255).
541    pub g: u8,
542    /// Blue channel (0-255).
543    pub b: u8,
544    /// Alpha channel (0 = fully transparent, 255 = fully opaque).
545    pub a: u8,
546}
547
548impl Color {
549    /// Opaque black: `rgba(0, 0, 0, 255)`.
550    pub const BLACK: Self = Self {
551        r: 0,
552        g: 0,
553        b: 0,
554        a: 255,
555    };
556    /// Opaque white: `rgba(255, 255, 255, 255)`.
557    pub const WHITE: Self = Self {
558        r: 255,
559        g: 255,
560        b: 255,
561        a: 255,
562    };
563    /// Opaque red: `rgba(255, 0, 0, 255)`.
564    pub const RED: Self = Self {
565        r: 255,
566        g: 0,
567        b: 0,
568        a: 255,
569    };
570    /// Opaque green: `rgba(0, 255, 0, 255)`.
571    pub const GREEN: Self = Self {
572        r: 0,
573        g: 255,
574        b: 0,
575        a: 255,
576    };
577    /// Opaque blue: `rgba(0, 0, 255, 255)`.
578    pub const BLUE: Self = Self {
579        r: 0,
580        g: 0,
581        b: 255,
582        a: 255,
583    };
584
585    /// Returns a copy of this color with a different alpha value.
586    ///
587    /// Useful for creating semi-transparent variants of existing colors without
588    /// constructing a new `Color` from scratch.
589    pub fn with_alpha(mut self, a: u8) -> Self {
590        self.a = a;
591        self
592    }
593}
594
595/// A solid color fill.
596///
597/// Used by [`PaintOp::DrawRect`] and [`PaintOp::DrawPath`] to fill shapes with
598/// a single color.
599#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
600pub struct Fill {
601    /// The fill color.
602    pub color: Color,
603}
604
605/// A colored stroke (outline) with a line width.
606///
607/// Used by [`PaintOp::DrawRect`] and [`PaintOp::DrawPath`] to draw shape borders.
608#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
609pub struct Stroke {
610    /// The stroke color.
611    pub color: Color,
612    /// The stroke width in logical pixels.
613    pub width: LayoutUnit,
614}
615
616impl std::hash::Hash for Stroke {
617    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
618        self.color.hash(state);
619        self.width.to_bits().hash(state);
620    }
621}
622
623/// A drop shadow rendered behind a rectangle.
624///
625/// Used by [`PaintOp::DrawRect`] to add depth and elevation effects.
626#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
627pub struct BoxShadow {
628    /// The shadow color (typically semi-transparent black).
629    pub color: Color,
630    /// The Gaussian blur radius in logical pixels. Larger values produce softer shadows.
631    pub blur_radius: LayoutUnit,
632    /// The horizontal and vertical offset of the shadow from the rectangle:
633    /// `(dx, dy)` in logical pixels.
634    pub offset: (LayoutUnit, LayoutUnit),
635}
636
637impl std::hash::Hash for BoxShadow {
638    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
639        self.color.hash(state);
640        self.blur_radius.to_bits().hash(state);
641        self.offset.0.to_bits().hash(state);
642        self.offset.1.to_bits().hash(state);
643    }
644}
645
646/// How an image scales to fit its layout box.
647///
648/// Equivalent to the CSS `object-fit` property.
649#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
650pub enum ImageFit {
651    /// Scale the image uniformly so it fits entirely within the box, preserving
652    /// aspect ratio. The image may be letter-boxed.
653    Contain,
654    /// Scale the image uniformly so it covers the entire box, preserving aspect
655    /// ratio. Parts of the image may be clipped.
656    Cover,
657    /// Stretch the image to fill the box exactly, ignoring aspect ratio.
658    Fill,
659    /// Display the image at its natural size, without any scaling.
660    None,
661}
662
663/// Styling properties for a run of text.
664///
665/// `TextStyle` controls how a segment of text is rendered: font size, color, underline,
666/// and an optional background highlight (used for search-match highlighting, error
667/// squiggles, etc.).
668///
669/// # Example
670///
671/// ```rust
672/// use fission_ir::op::{TextStyle, Color};
673///
674/// let style = TextStyle {
675///     font_size: 14.0,
676///     color: Color::BLACK,
677///     underline: false,
678///     background_color: None,
679/// };
680/// ```
681#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
682pub struct TextStyle {
683    /// Font size in logical pixels.
684    pub font_size: LayoutUnit,
685    /// Text foreground color.
686    pub color: Color,
687    /// Whether to draw an underline beneath the text.
688    pub underline: bool,
689    /// Optional background highlight color for this run (find matches, error
690    /// squiggles, selected text, etc.).
691    pub background_color: Option<Color>,
692}
693
694impl std::hash::Hash for TextStyle {
695    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
696        self.font_size.to_bits().hash(state);
697        self.color.hash(state);
698        self.underline.hash(state);
699        self.background_color.hash(state);
700    }
701}
702
703/// A contiguous run of text with a uniform style.
704///
705/// Rich text is represented as a sequence of `TextRun`s. Each run has its own
706/// [`TextStyle`], so different parts of a paragraph can have different colors,
707/// sizes, or underline states.
708///
709/// # Example
710///
711/// ```rust
712/// use fission_ir::op::{TextRun, TextStyle, Color};
713///
714/// let runs = vec![
715///     TextRun {
716///         text: "Hello, ".into(),
717///         style: TextStyle { font_size: 14.0, color: Color::BLACK, underline: false, background_color: None },
718///     },
719///     TextRun {
720///         text: "world!".into(),
721///         style: TextStyle { font_size: 14.0, color: Color::BLUE, underline: true, background_color: None },
722///     },
723/// ];
724/// ```
725#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
726pub struct TextRun {
727    /// The text content of this run.
728    pub text: String,
729    /// The style applied to every character in this run.
730    pub style: TextStyle,
731}
732
733/// A paint operation that draws something on screen.
734///
735/// Paint nodes do not participate in layout sizing -- their visual output is
736/// painted into the bounding box determined by their parent layout node. The
737/// renderer walks paint ops to build the final [`DisplayList`](fission_render).
738#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
739pub enum PaintOp {
740    /// Draws a filled and/or stroked rectangle with optional rounded corners and shadow.
741    ///
742    /// This is the workhorse of the paint pipeline -- backgrounds, borders, cards,
743    /// buttons, and dividers all compile down to `DrawRect`.
744    DrawRect {
745        /// The interior fill color. `None` means the rectangle has no fill (transparent).
746        fill: Option<Fill>,
747        /// The border stroke. `None` means no border.
748        stroke: Option<Stroke>,
749        /// Corner radius in logical pixels. `0.0` means sharp corners.
750        corner_radius: LayoutUnit,
751        /// An optional drop shadow behind the rectangle.
752        shadow: Option<BoxShadow>,
753    },
754    /// Draws a single-style text string.
755    ///
756    /// Use this for simple labels where the entire string shares one style. For
757    /// mixed-style text (e.g., syntax highlighting), use [`DrawRichText`](PaintOp::DrawRichText).
758    DrawText {
759        /// The text content to render.
760        text: String,
761        /// Font size in logical pixels.
762        size: LayoutUnit,
763        /// Text foreground color.
764        color: Color,
765        /// Whether to underline the text.
766        underline: bool,
767        /// If set, the renderer draws a text cursor at this byte index.
768        caret_index: Option<usize>,
769    },
770    /// Draws multi-style (rich) text composed of [`TextRun`]s.
771    ///
772    /// Each run can have a different font size, color, underline, and background
773    /// highlight. Used for code editors, formatted messages, and any text where
774    /// inline styling varies.
775    DrawRichText {
776        /// The styled text runs, in order.
777        runs: Vec<TextRun>,
778        /// If set, the renderer draws a text cursor at this byte index
779        /// (relative to the concatenated run text).
780        caret_index: Option<usize>,
781    },
782    /// Draws a raster image from a URI or asset path.
783    DrawImage {
784        /// The image source: a file path, HTTP URL, or asset identifier.
785        source: String,
786        /// How the image scales to fit its layout box.
787        fit: ImageFit,
788    },
789    /// Draws an SVG-style path string, optionally filled and/or stroked.
790    ///
791    /// The `path` uses SVG path data syntax (e.g., `"M 0 0 L 10 10 Z"`).
792    DrawPath {
793        /// SVG path data string.
794        path: String,
795        /// Optional fill color for the path interior.
796        fill: Option<Fill>,
797        /// Optional stroke for the path outline.
798        stroke: Option<Stroke>,
799    },
800    /// Draws inline SVG content, optionally overriding fill and stroke colors.
801    DrawSvg {
802        /// The raw SVG markup as a string.
803        content: String,
804        /// Optional fill color override applied to the SVG elements.
805        fill: Option<Fill>,
806        /// Optional stroke color override applied to the SVG elements.
807        stroke: Option<Stroke>,
808    },
809}
810
811impl std::hash::Hash for PaintOp {
812    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
813        match self {
814            Self::DrawRect { fill, stroke, corner_radius, shadow } => {
815                0.hash(state); fill.hash(state); stroke.hash(state);
816                corner_radius.to_bits().hash(state); shadow.hash(state);
817            }
818            Self::DrawText { text, size, color, underline, caret_index } => {
819                1.hash(state); text.hash(state); size.to_bits().hash(state);
820                color.hash(state); underline.hash(state); caret_index.hash(state);
821            }
822            Self::DrawRichText { runs, caret_index } => {
823                2.hash(state); runs.hash(state); caret_index.hash(state);
824            }
825            Self::DrawImage { source, fit } => {
826                3.hash(state); source.hash(state); fit.hash(state);
827            }
828            Self::DrawPath { path, fill, stroke } => {
829                4.hash(state); path.hash(state); fill.hash(state); stroke.hash(state);
830            }
831            Self::DrawSvg { content, fill, stroke } => {
832                5.hash(state); content.hash(state); fill.hash(state); stroke.hash(state);
833            }
834        }
835    }
836}