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