Skip to main content

fission_core/ui/widgets/
container.rs

1use crate::internal::InternalLower;
2use crate::lowering::{InternalIrBuilder, InternalLoweringCx};
3use crate::ui::Widget;
4use fission_ir::{
5    op::{
6        BackdropFilter, BoxAlignment, BoxGridPlacement, BoxPosition, BoxShadow, BoxStyle, Color,
7        Fill, GridPlacement, LayoutOp, Length, Op, OrderedLayoutUnit, Overflow, PaintOp, Stroke,
8    },
9    CompositeStyle, WidgetId,
10};
11use serde::{Deserialize, Serialize};
12
13use super::split_box_margin;
14
15/// The universal wrapper widget: typed box layout, background fill, border,
16/// padding, margin, overflow, aspect ratio, positioning, and shadow on one
17/// child.
18///
19/// `Container` is the workhorse of layout composition. Use it whenever you
20/// need to add visual decoration or spacing around a child widget.
21///
22/// # Example
23///
24/// ```rust,ignore
25/// const CARD_MIN_WIDTH: f32 = 280.0;
26/// const CARD_MAX_WIDTH: f32 = 520.0;
27///
28/// Container::new(Text::new("Card body"))
29///     .bg(theme.tokens.colors.surface)
30///     .border(theme.tokens.colors.border, 1.0)
31///     .border_radius(theme.tokens.radii.large)
32///     .padding_lengths(Length::all(Length::points(theme.tokens.spacing.l)))
33///     .width_length(Length::clamp(
34///         Length::points(CARD_MIN_WIDTH),
35///         Length::percent(42.0),
36///         Length::points(CARD_MAX_WIDTH),
37///     ))
38///     .flex_grow(1.0)
39/// ```
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct Container {
42    /// Explicit node identity.
43    pub id: Option<WidgetId>,
44    /// The single child widget.
45    pub child: Option<Widget>,
46
47    // -- Layout constraints --
48    /// Fixed width in layout points.
49    pub width: Option<f32>,
50    /// Fixed height in layout points.
51    pub height: Option<f32>,
52    /// Minimum width constraint.
53    pub min_width: Option<f32>,
54    /// Maximum width constraint.
55    pub max_width: Option<f32>,
56    /// Minimum height constraint.
57    pub min_height: Option<f32>,
58    /// Maximum height constraint.
59    pub max_height: Option<f32>,
60    /// Padding `[left, right, top, bottom]`.
61    pub padding: [f32; 4],
62    /// Flex grow factor (how much extra space this container absorbs).
63    pub flex_grow: f32,
64    /// Flex shrink factor (how much this container shrinks when space is tight).
65    pub flex_shrink: f32,
66    /// Declarative sizing, padding, aspect ratio, and overflow.
67    #[serde(default)]
68    pub box_style: BoxStyle,
69    /// Outer spacing `[left, right, top, bottom]`.
70    #[serde(default)]
71    pub margin: [f32; 4],
72
73    // -- Visual style --
74    /// Background fill.
75    pub background_fill: Option<Fill>,
76    /// Legacy background fill colour.
77    pub background_color: Option<Color>,
78    /// Border stroke colour.
79    pub border_color: Option<Color>,
80    /// Border stroke width in layout points.
81    pub border_width: f32,
82    /// Corner radius for rounded corners.
83    pub border_radius: f32,
84    /// Optional drop shadow.
85    pub shadow: Option<BoxShadow>,
86    /// Additional shadows drawn behind the container in order.
87    pub shadows: Vec<BoxShadow>,
88    /// Filter applied to content painted behind this container.
89    pub backdrop_filter: Option<BackdropFilter>,
90}
91
92impl Default for Container {
93    fn default() -> Self {
94        Self {
95            id: None,
96            child: None,
97            width: None,
98            height: None,
99            min_width: None,
100            max_width: None,
101            min_height: None,
102            max_height: None,
103            padding: [0.0; 4],
104            flex_grow: 0.0,
105            flex_shrink: 1.0,
106            box_style: BoxStyle::default(),
107            margin: [0.0; 4],
108            background_fill: None,
109            background_color: None,
110            border_color: None,
111            border_width: 0.0,
112            border_radius: 0.0,
113            shadow: None,
114            shadows: Vec::new(),
115            backdrop_filter: None,
116        }
117    }
118}
119impl Container {
120    pub fn new(child: impl Into<Widget>) -> Self {
121        let mut container = Self {
122            child: Some(child.into()),
123            ..Default::default()
124        };
125        container.box_style.alignment = BoxAlignment::Stretch;
126        container
127    }
128
129    pub fn size(mut self, w: f32, h: f32) -> Self {
130        self.width = Some(w);
131        self.height = Some(h);
132        self
133    }
134
135    pub fn width(mut self, w: f32) -> Self {
136        self.width = Some(w);
137        self
138    }
139
140    /// Sets a typed preferred width.
141    pub fn width_length(mut self, width: Length) -> Self {
142        self.box_style.width = Some(width);
143        self
144    }
145
146    /// Sets a typed preferred height.
147    pub fn height_length(mut self, height: Length) -> Self {
148        self.box_style.height = Some(height);
149        self
150    }
151
152    /// Sets a typed minimum width.
153    pub fn min_width_length(mut self, width: Length) -> Self {
154        self.box_style.min_width = Some(width);
155        self
156    }
157
158    /// Sets a typed maximum width.
159    pub fn max_width_length(mut self, width: Length) -> Self {
160        self.box_style.max_width = Some(width);
161        self
162    }
163
164    /// Sets a typed minimum height.
165    pub fn min_height_length(mut self, height: Length) -> Self {
166        self.box_style.min_height = Some(height);
167        self
168    }
169
170    /// Sets a typed maximum height.
171    pub fn max_height_length(mut self, height: Length) -> Self {
172        self.box_style.max_height = Some(height);
173        self
174    }
175
176    pub fn height(mut self, h: f32) -> Self {
177        self.height = Some(h);
178        self
179    }
180
181    pub fn min_width(mut self, w: f32) -> Self {
182        self.min_width = Some(w);
183        self
184    }
185
186    pub fn max_width(mut self, w: f32) -> Self {
187        self.max_width = Some(w);
188        self
189    }
190
191    pub fn min_height(mut self, h: f32) -> Self {
192        self.min_height = Some(h);
193        self
194    }
195
196    pub fn max_height(mut self, h: f32) -> Self {
197        self.max_height = Some(h);
198        self
199    }
200
201    pub fn padding_all(mut self, p: f32) -> Self {
202        self.padding = [p; 4];
203        self
204    }
205
206    pub fn padding(mut self, padding: [f32; 4]) -> Self {
207        self.padding = padding;
208        self
209    }
210
211    /// Sets typed `[left, right, top, bottom]` padding.
212    pub fn padding_lengths(mut self, padding: [Length; 4]) -> Self {
213        self.box_style.padding = Some(padding);
214        self
215    }
216
217    /// Sets equal point-based margin on every edge.
218    pub fn margin_all(mut self, margin: f32) -> Self {
219        self.margin = [margin; 4];
220        self
221    }
222
223    /// Sets point-based `[left, right, top, bottom]` margin.
224    pub fn margin(mut self, margin: [f32; 4]) -> Self {
225        self.margin = margin;
226        self
227    }
228
229    /// Sets typed `[left, right, top, bottom]` margin.
230    pub fn margin_lengths(mut self, margin: [Length; 4]) -> Self {
231        self.box_style.margin = Some(margin);
232        self
233    }
234
235    /// Aligns the child inside this container.
236    pub fn align_child(mut self, alignment: BoxAlignment) -> Self {
237        self.box_style.alignment = alignment;
238        self
239    }
240
241    /// Sets a non-negative width-to-height ratio.
242    pub fn aspect_ratio(mut self, ratio: f32) -> Self {
243        self.box_style.aspect_ratio = Some(OrderedLayoutUnit(ratio.max(0.0)));
244        self
245    }
246
247    /// Absolutely positions this container with point-based offsets.
248    pub fn positioned(
249        mut self,
250        left: Option<f32>,
251        top: Option<f32>,
252        right: Option<f32>,
253        bottom: Option<f32>,
254    ) -> Self {
255        self.box_style.position = Some(BoxPosition {
256            left: left.map(Length::Points),
257            top: top.map(Length::Points),
258            right: right.map(Length::Points),
259            bottom: bottom.map(Length::Points),
260        });
261        self
262    }
263
264    /// Positions this box with typed offsets relative to its positioned ancestor.
265    pub fn positioned_lengths(
266        mut self,
267        left: Option<Length>,
268        top: Option<Length>,
269        right: Option<Length>,
270        bottom: Option<Length>,
271    ) -> Self {
272        self.box_style.position = Some(BoxPosition {
273            left,
274            top,
275            right,
276            bottom,
277        });
278        self
279    }
280
281    /// Places this container at a one-based grid row and column.
282    pub fn grid_cell(mut self, row: i16, column: i16) -> Self {
283        self.box_style.grid = Some(BoxGridPlacement {
284            row_start: GridPlacement::Line(row),
285            col_start: GridPlacement::Line(column),
286            ..Default::default()
287        });
288        self
289    }
290
291    /// Spans this container across parent grid rows and columns.
292    pub fn grid_span(mut self, rows: u16, columns: u16) -> Self {
293        let placement = self.box_style.grid.get_or_insert_default();
294        placement.row_end = GridPlacement::Span(rows.max(1));
295        placement.col_end = GridPlacement::Span(columns.max(1));
296        self
297    }
298
299    /// Clips painting and descendants to this container's bounds.
300    pub fn clip_overflow(mut self, clip: bool) -> Self {
301        self.box_style.overflow = if clip {
302            Overflow::Clip
303        } else {
304            Overflow::Visible
305        };
306        self
307    }
308
309    pub fn flex_grow(mut self, grow: f32) -> Self {
310        self.flex_grow = grow;
311        self.box_style.flex_grow = Some(OrderedLayoutUnit(grow));
312        self
313    }
314
315    pub fn flex_shrink(mut self, shrink: f32) -> Self {
316        self.flex_shrink = shrink;
317        self.box_style.flex_shrink = Some(OrderedLayoutUnit(shrink));
318        self
319    }
320
321    pub fn bg(mut self, color: Color) -> Self {
322        self.background_fill = Some(Fill::Solid(color));
323        self.background_color = Some(color);
324        self
325    }
326
327    pub fn bg_fill(mut self, fill: Fill) -> Self {
328        self.background_fill = Some(fill);
329        self.background_color = None;
330        self
331    }
332
333    pub fn border(mut self, color: Color, width: f32) -> Self {
334        self.border_color = Some(color);
335        self.border_width = width;
336        self
337    }
338
339    pub fn border_radius(mut self, radius: f32) -> Self {
340        self.border_radius = radius;
341        self
342    }
343
344    pub fn shadow(mut self, shadow: BoxShadow) -> Self {
345        self.shadow = Some(shadow);
346        self
347    }
348
349    pub fn shadows(mut self, shadows: Vec<BoxShadow>) -> Self {
350        self.shadows = shadows;
351        self
352    }
353
354    /// Blurs content behind this container, clipped to its rounded bounds.
355    pub fn backdrop_blur(mut self, sigma: f32) -> Self {
356        self.backdrop_filter = Some(BackdropFilter::Blur(sigma.max(0.0)));
357        self
358    }
359}
360
361impl InternalLower for Container {
362    fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
363        let id = self.id.map(Into::into).unwrap_or_else(|| cx.next_node_id());
364        cx.push_scope(id);
365
366        let mut children_ids = Vec::new();
367
368        if let Some(filter) = self.backdrop_filter {
369            let paint = InternalIrBuilder::new(
370                cx.next_node_id(),
371                Op::Paint(PaintOp::BackdropFilter {
372                    filter,
373                    corner_radius: self.border_radius,
374                }),
375            )
376            .build(cx);
377            children_ids.push(paint);
378        }
379
380        // 1. Background Layer (PaintOp -> AbsoluteFill)
381        if self.background_fill.is_some()
382            || self.background_color.is_some()
383            || self.border_color.is_some()
384            || self.shadow.is_some()
385            || !self.shadows.is_empty()
386        {
387            for shadow in &self.shadows {
388                let paint = InternalIrBuilder::new(
389                    cx.next_node_id(),
390                    Op::Paint(PaintOp::DrawRect {
391                        fill: None,
392                        stroke: None,
393                        corner_radius: self.border_radius,
394                        shadow: Some(*shadow),
395                    }),
396                )
397                .build(cx);
398                children_ids.push(paint);
399            }
400            let paint = InternalIrBuilder::new(
401                cx.next_node_id(),
402                Op::Paint(PaintOp::DrawRect {
403                    fill: self
404                        .background_fill
405                        .clone()
406                        .or_else(|| self.background_color.map(Fill::Solid)),
407                    stroke: self.border_color.map(|c| Stroke {
408                        fill: Fill::Solid(c),
409                        width: self.border_width,
410                        dash_array: None,
411                        line_cap: fission_ir::op::LineCap::Butt,
412                        line_join: fission_ir::op::LineJoin::Miter,
413                    }),
414                    corner_radius: self.border_radius,
415                    shadow: self.shadow,
416                }),
417            )
418            .build(cx);
419            children_ids.push(paint);
420        }
421
422        // 2. Content Layer
423        if let Some(child) = &self.child {
424            children_ids.push(child.lower(cx));
425        }
426
427        cx.pop_scope();
428
429        let mut style = self.box_style.clone();
430        style.width = style.width.or(self.width.map(Length::Points));
431        style.height = style.height.or(self.height.map(Length::Points));
432        style.min_width = style.min_width.or(self.min_width.map(Length::Points));
433        style.max_width = style.max_width.or(self.max_width.map(Length::Points));
434        style.min_height = style.min_height.or(self.min_height.map(Length::Points));
435        style.max_height = style.max_height.or(self.max_height.map(Length::Points));
436        style.padding = style
437            .padding
438            .or_else(|| (self.padding != [0.0; 4]).then(|| self.padding.map(Length::Points)));
439        style.margin = style
440            .margin
441            .or_else(|| (self.margin != [0.0; 4]).then(|| self.margin.map(Length::Points)));
442        let margin_style = split_box_margin(&mut style);
443        let position = style.position.take();
444        let grid = style.grid.take();
445        let flex_grow = style
446            .flex_grow
447            .map(|value| value.0)
448            .unwrap_or(self.flex_grow);
449        let flex_shrink = style
450            .flex_shrink
451            .map(|value| value.0)
452            .unwrap_or(self.flex_shrink);
453
454        let mut layout = InternalIrBuilder::new(
455            id,
456            Op::Layout(LayoutOp::StyledBox {
457                style: style.clone(),
458                flex_grow,
459                flex_shrink,
460            }),
461        )
462        .composite(CompositeStyle {
463            clip_to_bounds: style.overflow == Overflow::Clip,
464            ..Default::default()
465        });
466
467        for cid in children_ids {
468            layout.add_child(cid);
469        }
470
471        let mut result = layout.build(cx);
472        if let Some(margin_style) = margin_style {
473            let mut outer = InternalIrBuilder::new(
474                cx.next_node_id(),
475                Op::Layout(LayoutOp::StyledBox {
476                    style: margin_style,
477                    flex_grow,
478                    flex_shrink,
479                }),
480            );
481            outer.add_child(result);
482            result = outer.build(cx);
483        }
484        if let Some(position) = position {
485            let mut outer = InternalIrBuilder::new(
486                cx.next_node_id(),
487                Op::Layout(LayoutOp::PositionedLengths {
488                    left: position.left,
489                    top: position.top,
490                    right: position.right,
491                    bottom: position.bottom,
492                    width: None,
493                    height: None,
494                }),
495            );
496            outer.add_child(result);
497            result = outer.build(cx);
498        }
499        if let Some(grid) = grid {
500            let mut outer = InternalIrBuilder::new(
501                cx.next_node_id(),
502                Op::Layout(LayoutOp::GridItem {
503                    row_start: grid.row_start,
504                    row_end: grid.row_end,
505                    col_start: grid.col_start,
506                    col_end: grid.col_end,
507                }),
508            );
509            outer.add_child(result);
510            result = outer.build(cx);
511        }
512        result
513    }
514}