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}