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