Skip to main content

fission_core/ui/widgets/
semantics_region.rs

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