Skip to main content

fission_core/ui/widgets/
semantics_region.rs

1use crate::internal::InternalLower;
2use crate::lowering::InternalIrBuilder;
3use crate::ui::Widget;
4use crate::ActionEnvelope;
5use fission_ir::{ActionEntry, ActionSet, Op, Role, Semantics, WidgetId};
6use serde::{Deserialize, Serialize};
7
8/// Wraps a subtree in an explicit semantics node.
9///
10/// Use `SemanticsRegion` when a shell or renderer needs a stable semantic
11/// target around an otherwise normal widget subtree. For example, the server
12/// shell uses semantic regions as mount points for focused browser islands.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct SemanticsRegion {
15    /// Explicit node identity for the region.
16    pub id: Option<WidgetId>,
17    /// Stable semantic identifier exposed to renderers and shell adapters.
18    pub identifier: Option<String>,
19    /// Optional accessible label for the region.
20    pub label: Option<String>,
21    /// Optional semantic value exposed to shells and renderers.
22    pub value: Option<String>,
23    /// Semantic role. Defaults to a generic region.
24    pub role: Role,
25    /// Actions attached to the semantic region.
26    pub actions: ActionSet,
27    /// Wrapped child subtree.
28    pub child: Option<Widget>,
29}
30
31impl SemanticsRegion {
32    /// Creates a semantic wrapper around an existing child node.
33    ///
34    /// Use builder methods to add a stable identifier, accessible label, role,
35    /// or action metadata before converting the region into a `Widget`.
36    pub fn new(child: impl Into<Widget>) -> Self {
37        Self {
38            child: Some(child.into()),
39            ..Default::default()
40        }
41    }
42
43    /// Sets an explicit node id for the region.
44    ///
45    /// This is useful when generated browser artifacts need to send actions
46    /// back to a known mount point. Prefer leaving it unset unless the shell or
47    /// renderer requires a stable id.
48    /// Sets the semantic identifier exposed to shells and HTML renderers.
49    ///
50    /// Identifiers are intended to be stable within a route. They are used by
51    /// tests, accessibility bridges, and progressive enhancement code to find
52    /// the right semantic region without depending on generated DOM structure.
53    pub fn identifier(mut self, identifier: impl Into<String>) -> Self {
54        self.identifier = Some(identifier.into());
55        self
56    }
57
58    /// Sets the accessible label for the semantic region.
59    ///
60    /// Use this when the wrapped child does not already expose enough text for
61    /// assistive technologies to describe the region or control clearly.
62    pub fn label(mut self, label: impl Into<String>) -> Self {
63        self.label = Some(label.into());
64        self
65    }
66
67    /// Sets the semantic value exposed to shells and HTML renderers.
68    ///
69    /// Most semantic regions do not need a value. It is useful for renderer
70    /// extensions where the stable identifier names the behavior and the value
71    /// carries structured, serializable configuration.
72    pub fn value(mut self, value: impl Into<String>) -> Self {
73        self.value = Some(value.into());
74        self
75    }
76
77    /// Sets the semantic role of the region.
78    ///
79    /// Choose the role that matches the user-visible behavior of the wrapped
80    /// child. For example, a styled region that behaves like a button should use
81    /// `Role::Button` and expose a default action.
82    pub fn role(mut self, role: Role) -> Self {
83        self.role = role;
84        self
85    }
86
87    /// Attaches the action that should run when the region is activated.
88    ///
89    /// This is the semantic equivalent of a button press. It lets renderers
90    /// expose activation consistently across mouse, keyboard, accessibility,
91    /// and browser-island event paths.
92    pub fn default_action(mut self, action: ActionEnvelope) -> Self {
93        self.actions.entries.push(ActionEntry {
94            trigger: fission_ir::semantics::ActionTrigger::Default,
95            action_id: action.id.as_u128(),
96            payload_data: Some(action.payload),
97        });
98        self
99    }
100}
101
102impl Default for SemanticsRegion {
103    fn default() -> Self {
104        Self {
105            id: None,
106            identifier: None,
107            label: None,
108            value: None,
109            role: Role::Generic,
110            actions: ActionSet::default(),
111            child: None,
112        }
113    }
114}
115
116impl InternalLower for SemanticsRegion {
117    fn lower(&self, cx: &mut crate::lowering::InternalLoweringCx) -> WidgetId {
118        let id = self.id.map(Into::into).unwrap_or_else(|| cx.next_node_id());
119        cx.push_scope(id);
120        let semantics = Semantics {
121            role: self.role,
122            identifier: self.identifier.clone(),
123            label: self.label.clone(),
124            value: self.value.clone(),
125            actions: self.actions.clone(),
126            ..Default::default()
127        };
128        let child_id = self.child.as_ref().map(|child| child.lower(cx));
129        let mut builder = InternalIrBuilder::new(id, Op::Semantics(semantics));
130        if let Some(child_id) = child_id {
131            builder.add_child(child_id);
132        }
133        let node_id = builder.build(cx);
134        cx.pop_scope();
135        node_id
136    }
137}