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 /// 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<Widget>,
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 into a `Widget`.
34 pub fn new(child: impl Into<Widget>) -> Self {
35 Self {
36 child: Some(child.into()),
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 /// Sets the semantic identifier exposed to shells and HTML renderers.
47 ///
48 /// Identifiers are intended to be stable within a route. They are used by
49 /// tests, accessibility bridges, and progressive enhancement code to find
50 /// the right semantic region without depending on generated DOM structure.
51 pub fn identifier(mut self, identifier: impl Into<String>) -> Self {
52 self.identifier = Some(identifier.into());
53 self
54 }
55
56 /// Sets the accessible label for the semantic region.
57 ///
58 /// Use this when the wrapped child does not already expose enough text for
59 /// assistive technologies to describe the region or control clearly.
60 pub fn label(mut self, label: impl Into<String>) -> Self {
61 self.label = Some(label.into());
62 self
63 }
64
65 /// Sets the semantic role of the region.
66 ///
67 /// Choose the role that matches the user-visible behavior of the wrapped
68 /// child. For example, a styled region that behaves like a button should use
69 /// `Role::Button` and expose a default action.
70 pub fn role(mut self, role: Role) -> Self {
71 self.role = role;
72 self
73 }
74
75 /// Attaches the action that should run when the region is activated.
76 ///
77 /// This is the semantic equivalent of a button press. It lets renderers
78 /// expose activation consistently across mouse, keyboard, accessibility,
79 /// and browser-island event paths.
80 pub fn default_action(mut self, action: ActionEnvelope) -> Self {
81 self.actions.entries.push(ActionEntry {
82 trigger: fission_ir::semantics::ActionTrigger::Default,
83 action_id: action.id.as_u128(),
84 payload_data: Some(action.payload),
85 });
86 self
87 }
88}
89
90impl Default for SemanticsRegion {
91 fn default() -> Self {
92 Self {
93 id: None,
94 identifier: None,
95 label: None,
96 role: Role::Generic,
97 actions: ActionSet::default(),
98 child: None,
99 }
100 }
101}
102
103impl InternalLower for SemanticsRegion {
104 fn lower(&self, cx: &mut crate::lowering::InternalLoweringCx) -> WidgetId {
105 let id = self.id.map(Into::into).unwrap_or_else(|| cx.next_node_id());
106 cx.push_scope(id);
107 let semantics = Semantics {
108 role: self.role,
109 identifier: self.identifier.clone(),
110 label: self.label.clone(),
111 actions: self.actions.clone(),
112 ..Default::default()
113 };
114 let child_id = self.child.as_ref().map(|child| child.lower(cx));
115 let mut builder = InternalIrBuilder::new(id, Op::Semantics(semantics));
116 if let Some(child_id) = child_id {
117 builder.add_child(child_id);
118 }
119 let node_id = builder.build(cx);
120 cx.pop_scope();
121 node_id
122 }
123}