Skip to main content

fission_core/ui/widgets/
overlay.rs

1use crate::internal::InternalLower;
2use crate::lowering::{wrap_zstack_child, InternalIrBuilder, InternalLoweringCx};
3use crate::ui::{Text, TextContent, Widget};
4use fission_ir::{LayoutOp, Op, WidgetId};
5use serde::{Deserialize, Serialize};
6
7/// A widget that renders an overlay layer on top of its content.
8///
9/// The `content` is drawn first, then `overlay` is drawn on top, filling the
10/// same bounds via a [`ZStack`](super::ZStack) internally.
11///
12/// # Example
13///
14/// ```rust,ignore
15/// Overlay {
16///     content: main_content,
17///     overlay: loading_spinner,
18///     ..Default::default()
19/// }
20/// ```
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Overlay {
23    /// Explicit node identity.
24    pub id: Option<WidgetId>,
25    /// The primary content (drawn first / underneath).
26    pub content: Widget,
27    /// The overlay content (drawn second / on top).
28    pub overlay: Widget,
29}
30
31impl Overlay {}
32
33impl Default for Overlay {
34    fn default() -> Self {
35        Self {
36            id: None,
37            content: Text {
38                content: TextContent::Literal("".into()),
39                ..Default::default()
40            }
41            .into(),
42            overlay: Text {
43                content: TextContent::Literal("".into()),
44                ..Default::default()
45            }
46            .into(),
47        }
48    }
49}
50
51impl InternalLower for Overlay {
52    fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
53        let id = self.id.map(Into::into).unwrap_or_else(|| cx.next_node_id());
54
55        cx.push_scope(id);
56
57        // Build overlay child in its own scope to avoid ID collisions
58        // with the content tree.
59        let overlay_scope = cx.next_node_id();
60        cx.push_scope(overlay_scope);
61        let overlay_child_id = self.overlay.lower(cx);
62        cx.pop_scope();
63        let mut overlay_fill =
64            InternalIrBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::AbsoluteFill));
65        overlay_fill.add_child(overlay_child_id);
66        let overlay_fill_id = overlay_fill.build(cx);
67
68        // Stack container: content first, overlay second.
69        let stack_id = cx.next_node_id();
70        let content_id = self.content.lower(cx);
71        cx.push_scope(stack_id);
72        let content_wrapped = wrap_zstack_child(cx, content_id);
73        let overlay_wrapped = wrap_zstack_child(cx, overlay_fill_id);
74        cx.pop_scope();
75
76        let mut stack = InternalIrBuilder::new(stack_id, Op::Layout(LayoutOp::ZStack));
77        stack.add_child(content_wrapped);
78        stack.add_child(overlay_wrapped);
79        let stack_id = stack.build(cx);
80
81        // Ensure the stack fills available space so overlay AbsoluteFill can cover
82        // the full viewport even when content is small.
83        let mut stack_wrapper = InternalIrBuilder::new(
84            cx.next_node_id(),
85            Op::Layout(LayoutOp::Box {
86                width: None,
87                height: None,
88                min_width: None,
89                max_width: None,
90                min_height: None,
91                max_height: None,
92                padding: [0.0; 4],
93                flex_grow: 1.0,
94                flex_shrink: 1.0,
95                aspect_ratio: None,
96            }),
97        );
98        stack_wrapper.add_child(stack_id);
99        let stack_wrapper_id = stack_wrapper.build(cx);
100
101        // Wrap ZStack in a Flex container with flex_grow = 1.0
102        // Flex defaults to stretching children, unlike Box which centers.
103        let mut root = InternalIrBuilder::new(
104            id,
105            Op::Layout(LayoutOp::Flex {
106                direction: fission_ir::FlexDirection::Column,
107                wrap: fission_ir::FlexWrap::NoWrap,
108                flex_grow: 1.0,
109                flex_shrink: 1.0,
110                padding: [0.0; 4],
111                gap: None,
112                align_items: fission_ir::op::AlignItems::Stretch,
113                justify_content: fission_ir::op::JustifyContent::Start,
114            }),
115        );
116        root.add_child(stack_wrapper_id);
117
118        cx.pop_scope();
119
120        root.build(cx)
121    }
122}