1use super::custom_render::CustomRenderObject;
2use super::traits::{InternalLower, InternalLowerer};
3#[cfg(feature = "interactive-canvas")]
4use super::widgets::InteractiveViewer;
5use super::widgets::{
6 ActionScope, Align, Button, Checkbox, Clip, Column, Composite, Container, ContextMenuEntry,
7 ContextMenuRegion, FocusScope, GestureDetector, Grid, GridItem, Icon, Image, LazyColumn,
8 Overlay, Positioned, Pressable, Radio, Responsive, RichText, Row, SafeArea, Scroll,
9 SemanticsRegion, Slider, Spacer, Switch, Text, TextInput, Transform, Video, ZStack,
10};
11use crate::lowering::InternalLoweringCx;
12use fission_ir::{Op, StructuralOp, WidgetId};
13use serde::{Deserialize, Serialize};
14use std::ops::ControlFlow;
15use std::sync::Arc;
16
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct Widget {
19 kind: Box<WidgetKind>,
20}
21
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub enum WidgetKind {
24 Identified {
25 id: WidgetId,
26 child: Widget,
27 },
28 ActionScope(ActionScope),
29 Row(Row),
30 Column(Column),
31 Align(Align),
32 FocusScope(FocusScope),
33 Clip(Clip),
34 Text(Text),
35 RichText(RichText),
36 Transform(Transform),
37 #[cfg(feature = "interactive-canvas")]
38 InteractiveViewer(InteractiveViewer),
39 Button(Button),
40 Pressable(Pressable),
41 TextInput(TextInput),
42 Scroll(Scroll),
43 SemanticsRegion(SemanticsRegion),
44 Image(Image),
45 Video(Video),
46 ZStack(ZStack),
47 Overlay(Overlay),
48 Container(Container),
49 ContextMenuRegion(ContextMenuRegion),
50 GestureDetector(GestureDetector),
51 Grid(Grid),
52 GridItem(GridItem),
53 Responsive(Responsive),
54 Checkbox(Checkbox),
55 Switch(Switch),
56 Radio(Radio),
57 SafeArea(SafeArea),
58 Positioned(Positioned),
59 Spacer(Spacer),
60 Slider(Slider),
61 LazyColumn(LazyColumn),
62 Icon(Icon),
63 Composite(Composite),
64 Custom(InternalRenderNode),
65}
66
67impl Widget {
68 const CHILD_ROLE: u32 = 0xF155_2000;
69
70 pub fn kind(&self) -> &WidgetKind {
72 self.kind.as_ref()
73 }
74
75 pub fn visit(&self, visitor: &mut impl FnMut(&Widget) -> ControlFlow<()>) -> ControlFlow<()> {
81 visitor(self)?;
82 match self.kind.as_ref() {
83 WidgetKind::Identified { child, .. }
84 | WidgetKind::ActionScope(ActionScope { child, .. })
85 | WidgetKind::Align(Align { child, .. })
86 | WidgetKind::Clip(Clip { child, .. })
87 | WidgetKind::Transform(Transform { child, .. })
88 | WidgetKind::Pressable(Pressable { child, .. })
89 | WidgetKind::GestureDetector(GestureDetector { child, .. })
90 | WidgetKind::GridItem(GridItem { child, .. })
91 | WidgetKind::SafeArea(SafeArea { child, .. })
92 | WidgetKind::Composite(Composite { child, .. }) => child.visit(visitor),
93 WidgetKind::Row(Row { children, .. })
94 | WidgetKind::Column(Column { children, .. })
95 | WidgetKind::FocusScope(FocusScope { children, .. })
96 | WidgetKind::ZStack(ZStack { children, .. })
97 | WidgetKind::Grid(Grid { children, .. })
98 | WidgetKind::LazyColumn(LazyColumn { children, .. }) => {
99 for child in children {
100 child.visit(visitor)?;
101 }
102 ControlFlow::Continue(())
103 }
104 WidgetKind::Button(Button { child, .. })
105 | WidgetKind::Scroll(Scroll { child, .. })
106 | WidgetKind::SemanticsRegion(SemanticsRegion { child, .. })
107 | WidgetKind::Container(Container { child, .. })
108 | WidgetKind::Positioned(Positioned { child, .. }) => {
109 if let Some(child) = child {
110 child.visit(visitor)?;
111 }
112 ControlFlow::Continue(())
113 }
114 WidgetKind::Overlay(Overlay {
115 content, overlay, ..
116 }) => {
117 content.visit(visitor)?;
118 overlay.visit(visitor)
119 }
120 WidgetKind::ContextMenuRegion(ContextMenuRegion { child, menu, .. }) => {
121 child.visit(visitor)?;
122 for entry in &menu.items {
123 if let ContextMenuEntry::Item(item) = entry {
124 item.child.visit(visitor)?;
125 }
126 }
127 ControlFlow::Continue(())
128 }
129 WidgetKind::Responsive(Responsive {
130 cases, fallback, ..
131 }) => {
132 for case in cases {
133 case.child.visit(visitor)?;
134 }
135 fallback.visit(visitor)
136 }
137 WidgetKind::RichText(RichText { inline_widgets, .. }) => {
138 for inline in inline_widgets {
139 inline.widget.visit(visitor)?;
140 }
141 ControlFlow::Continue(())
142 }
143 WidgetKind::TextInput(TextInput { prefix, suffix, .. }) => {
144 if let Some(prefix) = prefix {
145 prefix.visit(visitor)?;
146 }
147 if let Some(suffix) = suffix {
148 suffix.visit(visitor)?;
149 }
150 ControlFlow::Continue(())
151 }
152 #[cfg(feature = "interactive-canvas")]
153 WidgetKind::InteractiveViewer(InteractiveViewer { child, .. }) => child.visit(visitor),
154 WidgetKind::Custom(_)
155 | WidgetKind::Text(_)
156 | WidgetKind::Image(_)
157 | WidgetKind::Video(_)
158 | WidgetKind::Checkbox(_)
159 | WidgetKind::Switch(_)
160 | WidgetKind::Radio(_)
161 | WidgetKind::Spacer(_)
162 | WidgetKind::Slider(_)
163 | WidgetKind::Icon(_) => ControlFlow::Continue(()),
164 }
165 }
166
167 pub(crate) fn with_id(self, id: WidgetId) -> Self {
168 let kind = match *self.kind {
169 WidgetKind::Identified { child, .. } => WidgetKind::Identified { id, child },
170 WidgetKind::ActionScope(w) => WidgetKind::Identified {
171 id,
172 child: Widget {
173 kind: Box::new(WidgetKind::ActionScope(w)),
174 },
175 },
176 WidgetKind::Custom(w) => WidgetKind::Identified {
177 id,
178 child: Widget {
179 kind: Box::new(WidgetKind::Custom(w)),
180 },
181 },
182 WidgetKind::Row(mut w) => {
183 w.id = Some(id);
184 WidgetKind::Row(w)
185 }
186 WidgetKind::Column(mut w) => {
187 w.id = Some(id);
188 WidgetKind::Column(w)
189 }
190 WidgetKind::Align(mut w) => {
191 w.id = Some(id);
192 WidgetKind::Align(w)
193 }
194 WidgetKind::FocusScope(mut w) => {
195 w.id = Some(id);
196 WidgetKind::FocusScope(w)
197 }
198 WidgetKind::Clip(mut w) => {
199 w.id = Some(id);
200 WidgetKind::Clip(w)
201 }
202 WidgetKind::Text(mut w) => {
203 w.id = Some(id);
204 WidgetKind::Text(w)
205 }
206 WidgetKind::RichText(mut w) => {
207 w.id = Some(id);
208 WidgetKind::RichText(w)
209 }
210 WidgetKind::Transform(mut w) => {
211 w.id = Some(id);
212 WidgetKind::Transform(w)
213 }
214 #[cfg(feature = "interactive-canvas")]
215 WidgetKind::InteractiveViewer(mut w) => {
216 w.id = Some(id);
217 WidgetKind::InteractiveViewer(w)
218 }
219 WidgetKind::Button(mut w) => {
220 w.id = Some(id);
221 WidgetKind::Button(w)
222 }
223 WidgetKind::Pressable(mut w) => {
224 w.id = Some(id);
225 WidgetKind::Pressable(w)
226 }
227 WidgetKind::TextInput(mut w) => {
228 w.id = Some(id);
229 WidgetKind::TextInput(w)
230 }
231 WidgetKind::Scroll(mut w) => {
232 w.id = Some(id);
233 WidgetKind::Scroll(w)
234 }
235 WidgetKind::SemanticsRegion(mut w) => {
236 w.id = Some(id);
237 WidgetKind::SemanticsRegion(w)
238 }
239 WidgetKind::Image(mut w) => {
240 w.id = Some(id);
241 WidgetKind::Image(w)
242 }
243 WidgetKind::Video(mut w) => {
244 w.id = Some(id);
245 WidgetKind::Video(w)
246 }
247 WidgetKind::ZStack(mut w) => {
248 w.id = Some(id);
249 WidgetKind::ZStack(w)
250 }
251 WidgetKind::Overlay(mut w) => {
252 w.id = Some(id);
253 WidgetKind::Overlay(w)
254 }
255 WidgetKind::Container(mut w) => {
256 w.id = Some(id);
257 WidgetKind::Container(w)
258 }
259 WidgetKind::ContextMenuRegion(mut w) => {
260 w.id = Some(id);
261 WidgetKind::ContextMenuRegion(w)
262 }
263 WidgetKind::GestureDetector(mut w) => {
264 w.id = Some(id);
265 WidgetKind::GestureDetector(w)
266 }
267 WidgetKind::Grid(mut w) => {
268 w.id = Some(id);
269 WidgetKind::Grid(w)
270 }
271 WidgetKind::GridItem(mut w) => {
272 w.id = Some(id);
273 WidgetKind::GridItem(w)
274 }
275 WidgetKind::Responsive(mut w) => {
276 w.id = Some(id);
277 WidgetKind::Responsive(w)
278 }
279 WidgetKind::Checkbox(mut w) => {
280 w.id = Some(id);
281 WidgetKind::Checkbox(w)
282 }
283 WidgetKind::Switch(mut w) => {
284 w.id = Some(id);
285 WidgetKind::Switch(w)
286 }
287 WidgetKind::Radio(mut w) => {
288 w.id = Some(id);
289 WidgetKind::Radio(w)
290 }
291 WidgetKind::SafeArea(mut w) => {
292 w.id = Some(id);
293 WidgetKind::SafeArea(w)
294 }
295 WidgetKind::Positioned(mut w) => {
296 w.id = Some(id);
297 WidgetKind::Positioned(w)
298 }
299 WidgetKind::Spacer(mut w) => {
300 w.id = Some(id);
301 WidgetKind::Spacer(w)
302 }
303 WidgetKind::Slider(mut w) => {
304 w.id = Some(id);
305 WidgetKind::Slider(w)
306 }
307 WidgetKind::LazyColumn(mut w) => {
308 w.id = Some(id);
309 WidgetKind::LazyColumn(w)
310 }
311 WidgetKind::Icon(mut w) => {
312 w.id = Some(id);
313 WidgetKind::Icon(w)
314 }
315 WidgetKind::Composite(mut w) => {
316 w.id = Some(id);
317 WidgetKind::Composite(w)
318 }
319 };
320 Self {
321 kind: Box::new(kind),
322 }
323 }
324
325 pub fn id<I>(self, id: I) -> Self
326 where
327 I: Into<WidgetId>,
328 {
329 self.with_id(id.into())
330 }
331
332 pub(crate) fn custom(node: InternalRenderNode) -> Self {
333 Self {
334 kind: Box::new(WidgetKind::Custom(node)),
335 }
336 }
337
338 pub(crate) fn from_pressable_raw(pressable: Pressable) -> Self {
339 Self {
340 kind: Box::new(WidgetKind::Pressable(pressable)),
341 }
342 }
343
344 pub(crate) fn into_text(self) -> Result<Text, Self> {
345 match *self.kind {
346 WidgetKind::Text(text) => Ok(text),
347 kind => Err(Self {
348 kind: Box::new(kind),
349 }),
350 }
351 }
352
353 pub(crate) fn kind_name(&self) -> &'static str {
354 match &*self.kind {
355 WidgetKind::Identified { .. } => "Identified",
356 WidgetKind::ActionScope(_) => "ActionScope",
357 WidgetKind::Row(_) => "Row",
358 WidgetKind::Column(_) => "Column",
359 WidgetKind::Align(_) => "Align",
360 WidgetKind::FocusScope(_) => "FocusScope",
361 WidgetKind::Clip(_) => "Clip",
362 WidgetKind::Text(_) => "Text",
363 WidgetKind::RichText(_) => "RichText",
364 WidgetKind::Transform(_) => "Transform",
365 #[cfg(feature = "interactive-canvas")]
366 WidgetKind::InteractiveViewer(_) => "InteractiveViewer",
367 WidgetKind::Button(_) => "Button",
368 WidgetKind::Pressable(_) => "Pressable",
369 WidgetKind::TextInput(_) => "TextInput",
370 WidgetKind::Scroll(_) => "Scroll",
371 WidgetKind::SemanticsRegion(_) => "SemanticsRegion",
372 WidgetKind::Image(_) => "Image",
373 WidgetKind::Video(_) => "Video",
374 WidgetKind::ZStack(_) => "ZStack",
375 WidgetKind::Overlay(_) => "Overlay",
376 WidgetKind::Container(_) => "Container",
377 WidgetKind::ContextMenuRegion(_) => "ContextMenuRegion",
378 WidgetKind::GestureDetector(_) => "GestureDetector",
379 WidgetKind::Grid(_) => "Grid",
380 WidgetKind::GridItem(_) => "GridItem",
381 WidgetKind::Responsive(_) => "Responsive",
382 WidgetKind::Checkbox(_) => "Checkbox",
383 WidgetKind::Switch(_) => "Switch",
384 WidgetKind::Radio(_) => "Radio",
385 WidgetKind::SafeArea(_) => "SafeArea",
386 WidgetKind::Positioned(_) => "Positioned",
387 WidgetKind::Spacer(_) => "Spacer",
388 WidgetKind::Slider(_) => "Slider",
389 WidgetKind::LazyColumn(_) => "LazyColumn",
390 WidgetKind::Icon(_) => "Icon",
391 WidgetKind::Composite(_) => "Composite",
392 WidgetKind::Custom(_) => "Custom",
393 }
394 }
395
396 fn kind_discriminator(&self) -> u32 {
397 match &*self.kind {
400 WidgetKind::Identified { .. } => 1,
401 WidgetKind::ActionScope(_) => 2,
402 WidgetKind::Row(_) => 3,
403 WidgetKind::Column(_) => 4,
404 WidgetKind::Align(_) => 5,
405 WidgetKind::FocusScope(_) => 6,
406 WidgetKind::Clip(_) => 7,
407 WidgetKind::Text(_) => 8,
408 WidgetKind::RichText(_) => 9,
409 WidgetKind::Transform(_) => 10,
410 #[cfg(feature = "interactive-canvas")]
411 WidgetKind::InteractiveViewer(_) => 11,
412 WidgetKind::Button(_) => 12,
413 WidgetKind::Pressable(_) => 13,
414 WidgetKind::TextInput(_) => 14,
415 WidgetKind::Scroll(_) => 15,
416 WidgetKind::SemanticsRegion(_) => 16,
417 WidgetKind::Image(_) => 17,
418 WidgetKind::Video(_) => 18,
419 WidgetKind::ZStack(_) => 19,
420 WidgetKind::Overlay(_) => 20,
421 WidgetKind::Container(_) => 21,
422 WidgetKind::ContextMenuRegion(_) => 22,
423 WidgetKind::GestureDetector(_) => 23,
424 WidgetKind::Grid(_) => 24,
425 WidgetKind::GridItem(_) => 25,
426 WidgetKind::Responsive(_) => 26,
427 WidgetKind::Checkbox(_) => 27,
428 WidgetKind::Switch(_) => 28,
429 WidgetKind::Radio(_) => 29,
430 WidgetKind::SafeArea(_) => 30,
431 WidgetKind::Positioned(_) => 31,
432 WidgetKind::Spacer(_) => 32,
433 WidgetKind::Slider(_) => 33,
434 WidgetKind::LazyColumn(_) => 34,
435 WidgetKind::Icon(_) => 35,
436 WidgetKind::Composite(_) => 36,
437 WidgetKind::Custom(_) => 37,
438 }
439 }
440
441 pub(crate) fn declared_id(&self) -> Option<WidgetId> {
442 match &*self.kind {
443 WidgetKind::Identified { id, .. } => Some(*id),
444 WidgetKind::ActionScope(_) => None,
445 WidgetKind::Custom(widget) => widget
446 .lowerer
447 .as_ref()
448 .and_then(|lowerer| lowerer.widget_id()),
449 WidgetKind::Row(widget) => widget.id,
450 WidgetKind::Column(widget) => widget.id,
451 WidgetKind::Align(widget) => widget.id,
452 WidgetKind::FocusScope(widget) => widget.id,
453 WidgetKind::Clip(widget) => widget.id,
454 WidgetKind::Text(widget) => widget.id,
455 WidgetKind::RichText(widget) => widget.id,
456 WidgetKind::Transform(widget) => widget.id,
457 #[cfg(feature = "interactive-canvas")]
458 WidgetKind::InteractiveViewer(widget) => widget.id,
459 WidgetKind::Button(widget) => widget.id,
460 WidgetKind::Pressable(widget) => widget.id,
461 WidgetKind::TextInput(widget) => widget.id,
462 WidgetKind::Scroll(widget) => widget.id,
463 WidgetKind::SemanticsRegion(widget) => widget.id,
464 WidgetKind::Image(widget) => widget.id,
465 WidgetKind::Video(widget) => widget.id,
466 WidgetKind::ZStack(widget) => widget.id,
467 WidgetKind::Overlay(widget) => widget.id,
468 WidgetKind::Container(widget) => widget.id,
469 WidgetKind::ContextMenuRegion(widget) => widget.id,
470 WidgetKind::GestureDetector(widget) => widget.id,
471 WidgetKind::Grid(widget) => widget.id,
472 WidgetKind::GridItem(widget) => widget.id,
473 WidgetKind::Responsive(widget) => widget.id,
474 WidgetKind::Checkbox(widget) => widget.id,
475 WidgetKind::Switch(widget) => widget.id,
476 WidgetKind::Radio(widget) => widget.id,
477 WidgetKind::SafeArea(widget) => widget.id,
478 WidgetKind::Positioned(widget) => widget.id,
479 WidgetKind::Spacer(widget) => widget.id,
480 WidgetKind::Slider(widget) => widget.id,
481 WidgetKind::LazyColumn(widget) => widget.id,
482 WidgetKind::Icon(widget) => widget.id,
483 WidgetKind::Composite(widget) => widget.id,
484 }
485 }
486
487 pub(crate) fn resolve_identities(self, root: WidgetId) -> Self {
488 self.resolve_identity(root)
489 }
490
491 fn resolve_identity(self, automatic_id: WidgetId) -> Self {
492 let resolved = if self.declared_id().is_some() {
493 self
494 } else {
495 self.with_id(automatic_id)
496 };
497 let parent = resolved.declared_id().unwrap_or(automatic_id);
498 resolved.resolve_descendants(parent)
499 }
500
501 fn resolve_descendants(self, parent: WidgetId) -> Self {
502 let child = |widget: Widget, slot: u32| {
503 let id = WidgetId::derived(
504 parent.as_u128(),
505 &[Self::CHILD_ROLE, slot, widget.kind_discriminator()],
506 );
507 widget.resolve_identity(id)
508 };
509 let children = |widgets: Vec<Widget>, first_slot: u32| {
510 widgets
511 .into_iter()
512 .enumerate()
513 .map(|(index, widget)| child(widget, first_slot + index as u32))
514 .collect()
515 };
516
517 let kind = match *self.kind {
518 WidgetKind::Identified {
519 id,
520 child: identified_child,
521 } => WidgetKind::Identified {
522 id,
523 child: identified_child.resolve_descendants(id),
528 },
529 WidgetKind::ActionScope(mut widget) => {
530 widget.child = child(widget.child, 0);
531 WidgetKind::ActionScope(widget)
532 }
533 WidgetKind::Row(mut widget) => {
534 widget.children = children(widget.children, 0);
535 WidgetKind::Row(widget)
536 }
537 WidgetKind::Column(mut widget) => {
538 widget.children = children(widget.children, 0);
539 WidgetKind::Column(widget)
540 }
541 WidgetKind::Align(mut widget) => {
542 widget.child = child(widget.child, 0);
543 WidgetKind::Align(widget)
544 }
545 WidgetKind::FocusScope(mut widget) => {
546 widget.children = children(widget.children, 0);
547 WidgetKind::FocusScope(widget)
548 }
549 WidgetKind::Clip(mut widget) => {
550 widget.child = child(widget.child, 0);
551 WidgetKind::Clip(widget)
552 }
553 WidgetKind::Text(widget) => WidgetKind::Text(widget),
554 WidgetKind::RichText(mut widget) => {
555 for (index, inline) in widget.inline_widgets.iter_mut().enumerate() {
556 inline.widget = child(inline.widget.clone(), index as u32);
557 }
558 WidgetKind::RichText(widget)
559 }
560 WidgetKind::Transform(mut widget) => {
561 widget.child = child(widget.child, 0);
562 WidgetKind::Transform(widget)
563 }
564 #[cfg(feature = "interactive-canvas")]
565 WidgetKind::InteractiveViewer(mut widget) => {
566 widget.child = child(widget.child, 0);
567 WidgetKind::InteractiveViewer(widget)
568 }
569 WidgetKind::Button(mut widget) => {
570 widget.child = widget.child.map(|value| child(value, 0));
571 WidgetKind::Button(widget)
572 }
573 WidgetKind::Pressable(mut widget) => {
574 widget.child = child(widget.child, 0);
575 WidgetKind::Pressable(widget)
576 }
577 WidgetKind::TextInput(mut widget) => {
578 widget.prefix = widget.prefix.map(|value| child(value, 0));
579 widget.suffix = widget.suffix.map(|value| child(value, 1));
580 WidgetKind::TextInput(widget)
581 }
582 WidgetKind::Scroll(mut widget) => {
583 widget.child = widget.child.map(|value| child(value, 0));
584 WidgetKind::Scroll(widget)
585 }
586 WidgetKind::SemanticsRegion(mut widget) => {
587 widget.child = widget.child.map(|value| child(value, 0));
588 WidgetKind::SemanticsRegion(widget)
589 }
590 WidgetKind::Image(widget) => WidgetKind::Image(widget),
591 WidgetKind::Video(widget) => WidgetKind::Video(widget),
592 WidgetKind::ZStack(mut widget) => {
593 widget.children = children(widget.children, 0);
594 WidgetKind::ZStack(widget)
595 }
596 WidgetKind::Overlay(mut widget) => {
597 widget.content = child(widget.content, 0);
598 widget.overlay = child(widget.overlay, 1);
599 WidgetKind::Overlay(widget)
600 }
601 WidgetKind::Container(mut widget) => {
602 widget.child = widget.child.map(|value| child(value, 0));
603 WidgetKind::Container(widget)
604 }
605 WidgetKind::ContextMenuRegion(mut widget) => {
606 widget.child = child(widget.child, 0);
607 for (index, entry) in widget.menu.items.iter_mut().enumerate() {
608 if let ContextMenuEntry::Item(item) = entry {
609 item.child = child(item.child.clone(), 1 + index as u32);
610 }
611 }
612 WidgetKind::ContextMenuRegion(widget)
613 }
614 WidgetKind::GestureDetector(mut widget) => {
615 widget.child = child(widget.child, 0);
616 WidgetKind::GestureDetector(widget)
617 }
618 WidgetKind::Grid(mut widget) => {
619 widget.children = children(widget.children, 0);
620 WidgetKind::Grid(widget)
621 }
622 WidgetKind::GridItem(mut widget) => {
623 widget.child = child(widget.child, 0);
624 WidgetKind::GridItem(widget)
625 }
626 WidgetKind::Responsive(mut widget) => {
627 for (index, case) in widget.cases.iter_mut().enumerate() {
628 case.child = child(case.child.clone(), index as u32);
629 }
630 widget.fallback = child(widget.fallback, u32::MAX);
631 WidgetKind::Responsive(widget)
632 }
633 WidgetKind::Checkbox(widget) => WidgetKind::Checkbox(widget),
634 WidgetKind::Switch(widget) => WidgetKind::Switch(widget),
635 WidgetKind::Radio(widget) => WidgetKind::Radio(widget),
636 WidgetKind::SafeArea(mut widget) => {
637 widget.child = child(widget.child, 0);
638 WidgetKind::SafeArea(widget)
639 }
640 WidgetKind::Positioned(mut widget) => {
641 widget.child = widget.child.map(|value| child(value, 0));
642 WidgetKind::Positioned(widget)
643 }
644 WidgetKind::Spacer(widget) => WidgetKind::Spacer(widget),
645 WidgetKind::Slider(widget) => WidgetKind::Slider(widget),
646 WidgetKind::LazyColumn(mut widget) => {
647 widget.children = children(widget.children, 0);
648 WidgetKind::LazyColumn(widget)
649 }
650 WidgetKind::Icon(widget) => WidgetKind::Icon(widget),
651 WidgetKind::Composite(mut widget) => {
652 widget.child = child(widget.child, 0);
653 WidgetKind::Composite(widget)
654 }
655 WidgetKind::Custom(widget) => WidgetKind::Custom(widget),
656 };
657
658 Self {
659 kind: Box::new(kind),
660 }
661 }
662
663 pub(crate) fn as_row(&self) -> Option<&Row> {
664 match &*self.kind {
665 WidgetKind::Identified { child, .. } => child.as_row(),
666 WidgetKind::Row(widget) => Some(widget),
667 _ => None,
668 }
669 }
670
671 pub(crate) fn as_column(&self) -> Option<&Column> {
672 match &*self.kind {
673 WidgetKind::Identified { child, .. } => child.as_column(),
674 WidgetKind::Column(widget) => Some(widget),
675 _ => None,
676 }
677 }
678
679 pub(crate) fn as_container(&self) -> Option<&Container> {
680 match &*self.kind {
681 WidgetKind::Identified { child, .. } => child.as_container(),
682 WidgetKind::Container(widget) => Some(widget),
683 _ => None,
684 }
685 }
686
687 pub(crate) fn as_scroll(&self) -> Option<&Scroll> {
688 match &*self.kind {
689 WidgetKind::Identified { child, .. } => child.as_scroll(),
690 WidgetKind::Scroll(widget) => Some(widget),
691 _ => None,
692 }
693 }
694
695 pub(crate) fn as_rich_text(&self) -> Option<&RichText> {
696 match &*self.kind {
697 WidgetKind::Identified { child, .. } => child.as_rich_text(),
698 WidgetKind::RichText(widget) => Some(widget),
699 _ => None,
700 }
701 }
702
703 pub(crate) fn as_text(&self) -> Option<&Text> {
704 match &*self.kind {
705 WidgetKind::Identified { child, .. } => child.as_text(),
706 WidgetKind::Text(widget) => Some(widget),
707 _ => None,
708 }
709 }
710
711 pub(crate) fn as_text_input(&self) -> Option<&TextInput> {
712 match &*self.kind {
713 WidgetKind::Identified { child, .. } => child.as_text_input(),
714 WidgetKind::TextInput(widget) => Some(widget),
715 _ => None,
716 }
717 }
718
719 pub(crate) fn as_button(&self) -> Option<&Button> {
720 match &*self.kind {
721 WidgetKind::Identified { child, .. } => child.as_button(),
722 WidgetKind::Button(widget) => Some(widget),
723 _ => None,
724 }
725 }
726
727 pub(crate) fn as_gesture_detector(&self) -> Option<&GestureDetector> {
728 match &*self.kind {
729 WidgetKind::Identified { child, .. } => child.as_gesture_detector(),
730 WidgetKind::GestureDetector(widget) => Some(widget),
731 _ => None,
732 }
733 }
734
735 pub(crate) fn as_zstack(&self) -> Option<&ZStack> {
736 match &*self.kind {
737 WidgetKind::Identified { child, .. } => child.as_zstack(),
738 WidgetKind::ZStack(widget) => Some(widget),
739 _ => None,
740 }
741 }
742
743 #[cfg(feature = "interactive-canvas")]
744 pub(crate) fn as_interactive_viewer(&self) -> Option<&InteractiveViewer> {
745 match &*self.kind {
746 WidgetKind::Identified { child, .. } => child.as_interactive_viewer(),
747 WidgetKind::InteractiveViewer(widget) => Some(widget),
748 _ => None,
749 }
750 }
751}
752
753pub trait WidgetIdExt: Into<Widget> + Sized {
760 fn id<I>(self, id: I) -> Widget
761 where
762 I: Into<WidgetId>,
763 {
764 let id = id.into();
765 crate::build::with_widget_id(id, || {
766 let widget: Widget = self.into();
767 widget.with_id(id)
768 })
769 }
770}
771
772impl<T> WidgetIdExt for T where T: Into<Widget> {}
773
774impl Widget {
775 pub(crate) fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
776 match &*self.kind {
777 WidgetKind::Identified { id, child } => {
778 cx.push_scope(*id);
779 let child_id = child.lower(cx);
780 cx.pop_scope();
781 let mut builder = crate::lowering::InternalIrBuilder::new(
782 (*id).into(),
783 Op::Structural(StructuralOp::Group {
784 stable_hash: id.as_u128() as u64,
785 }),
786 );
787 builder.add_child(child_id);
788 builder.build(cx)
789 }
790 WidgetKind::ActionScope(w) => w.lower(cx),
791 WidgetKind::Row(w) => w.lower(cx),
792 WidgetKind::Column(w) => w.lower(cx),
793 WidgetKind::Align(w) => w.lower(cx),
794 WidgetKind::FocusScope(w) => w.lower(cx),
795 WidgetKind::Clip(w) => w.lower(cx),
796 WidgetKind::Text(w) => w.lower(cx),
797 WidgetKind::RichText(w) => w.lower(cx),
798 WidgetKind::Transform(w) => w.lower(cx),
799 #[cfg(feature = "interactive-canvas")]
800 WidgetKind::InteractiveViewer(w) => w.lower(cx),
801 WidgetKind::Button(w) => w.lower(cx),
802 WidgetKind::Pressable(w) => w.lower(cx),
803 WidgetKind::TextInput(w) => w.lower(cx),
804 WidgetKind::Scroll(w) => w.lower(cx),
805 WidgetKind::SemanticsRegion(w) => w.lower(cx),
806 WidgetKind::Image(w) => w.lower(cx),
807 WidgetKind::Video(w) => w.lower(cx),
808 WidgetKind::ZStack(w) => w.lower(cx),
809 WidgetKind::Overlay(w) => w.lower(cx),
810 WidgetKind::Container(w) => w.lower(cx),
811 WidgetKind::ContextMenuRegion(w) => w.lower(cx),
812 WidgetKind::GestureDetector(w) => w.lower(cx),
813 WidgetKind::Grid(w) => w.lower(cx),
814 WidgetKind::GridItem(w) => w.lower(cx),
815 WidgetKind::Responsive(w) => w.lower(cx),
816 WidgetKind::Checkbox(w) => w.lower(cx),
817 WidgetKind::Switch(w) => w.lower(cx),
818 WidgetKind::Radio(w) => w.lower(cx),
819 WidgetKind::SafeArea(w) => w.lower(cx),
820 WidgetKind::Positioned(w) => w.lower(cx),
821 WidgetKind::Spacer(w) => w.lower(cx),
822 WidgetKind::Slider(w) => w.lower(cx),
823 WidgetKind::LazyColumn(w) => w.lower(cx),
824 WidgetKind::Icon(w) => w.lower(cx),
825 WidgetKind::Composite(w) => w.lower(cx),
826 WidgetKind::Custom(w) => {
827 let lowerer = w
828 .lowerer
829 .as_ref()
830 .expect("CustomWidget lowerer must be set");
831 let wrapper = lowerer.widget_id().unwrap_or_else(|| cx.next_node_id());
832 cx.push_scope(wrapper);
833 let child_id = lowerer.lower_dyn(cx);
834 cx.pop_scope();
835 let mut builder = crate::lowering::InternalIrBuilder::new(
836 wrapper,
837 Op::Structural(StructuralOp::Group {
838 stable_hash: lowerer.stable_key(),
839 }),
840 );
841 builder.add_child(child_id);
842 let node_id = builder.build(cx);
843
844 if let Some(render_obj) = &w.render_object {
850 let holder = crate::ui::custom_render::RenderObjectHolder(render_obj.clone());
851 let erased: fission_ir::AnyRenderObject = Arc::new(holder);
852 cx.ir.custom_render_objects.insert(node_id, erased);
853 }
854
855 node_id
856 }
857 }
858 }
859}
860
861impl From<Row> for Widget {
862 fn from(w: Row) -> Self {
863 Self {
864 kind: Box::new(WidgetKind::Row(w)),
865 }
866 }
867}
868impl From<ActionScope> for Widget {
869 fn from(w: ActionScope) -> Self {
870 Self {
871 kind: Box::new(WidgetKind::ActionScope(w)),
872 }
873 }
874}
875impl From<Column> for Widget {
876 fn from(w: Column) -> Self {
877 Self {
878 kind: Box::new(WidgetKind::Column(w)),
879 }
880 }
881}
882impl From<Align> for Widget {
883 fn from(w: Align) -> Self {
884 Self {
885 kind: Box::new(WidgetKind::Align(w)),
886 }
887 }
888}
889impl From<FocusScope> for Widget {
890 fn from(w: FocusScope) -> Self {
891 Self {
892 kind: Box::new(WidgetKind::FocusScope(w)),
893 }
894 }
895}
896impl From<Clip> for Widget {
897 fn from(w: Clip) -> Self {
898 Self {
899 kind: Box::new(WidgetKind::Clip(w)),
900 }
901 }
902}
903impl From<Text> for Widget {
904 fn from(w: Text) -> Self {
905 Self {
906 kind: Box::new(WidgetKind::Text(w)),
907 }
908 }
909}
910impl From<RichText> for Widget {
911 fn from(w: RichText) -> Self {
912 Self {
913 kind: Box::new(WidgetKind::RichText(w)),
914 }
915 }
916}
917impl From<Transform> for Widget {
918 fn from(w: Transform) -> Self {
919 Self {
920 kind: Box::new(WidgetKind::Transform(w)),
921 }
922 }
923}
924#[cfg(feature = "interactive-canvas")]
925impl From<InteractiveViewer> for Widget {
926 fn from(w: InteractiveViewer) -> Self {
927 Self {
928 kind: Box::new(WidgetKind::InteractiveViewer(w)),
929 }
930 }
931}
932impl From<Button> for Widget {
933 fn from(mut w: Button) -> Self {
934 if let Some(motion) = w.motion.take() {
935 let button_id = crate::build::current_widget_id()
936 .or(w.id)
937 .unwrap_or_else(|| WidgetId::explicit("fission.core.button.motion"));
938 w.id = Some(button_id);
939 let motion_id = WidgetId::derived(button_id.as_u128(), &[0xB0770]);
940 let tracks = motion.interaction_tracks(button_id);
941 let ripple = motion.ripple();
942 let base = Self {
943 kind: Box::new(WidgetKind::Button(w)),
944 };
945 let with_motion: Widget = if tracks.is_empty() {
946 base
947 } else {
948 crate::motion::Motion {
949 id: motion_id,
950 tracks,
951 child: base,
952 ..Default::default()
953 }
954 .into()
955 };
956 return if let Some(effect) = ripple {
957 crate::motion::RippleLayer {
958 id: WidgetId::derived(button_id.as_u128(), &[0xA11E]),
959 effect,
960 child: with_motion,
961 }
962 .into()
963 } else {
964 with_motion
965 };
966 }
967 Self {
968 kind: Box::new(WidgetKind::Button(w)),
969 }
970 }
971}
972impl From<TextInput> for Widget {
973 fn from(w: TextInput) -> Self {
974 Self {
975 kind: Box::new(WidgetKind::TextInput(w)),
976 }
977 }
978}
979impl From<Scroll> for Widget {
980 fn from(w: Scroll) -> Self {
981 Self {
982 kind: Box::new(WidgetKind::Scroll(w)),
983 }
984 }
985}
986impl From<SemanticsRegion> for Widget {
987 fn from(w: SemanticsRegion) -> Self {
988 Self {
989 kind: Box::new(WidgetKind::SemanticsRegion(w)),
990 }
991 }
992}
993impl From<Image> for Widget {
994 fn from(w: Image) -> Self {
995 Self {
996 kind: Box::new(WidgetKind::Image(w)),
997 }
998 }
999}
1000impl From<Video> for Widget {
1001 fn from(w: Video) -> Self {
1002 let node_id = crate::build::current_widget_id()
1003 .or(w.id)
1004 .unwrap_or_else(|| fission_ir::WidgetId::explicit(&w.source.key()));
1005 crate::build::try_register_video(crate::registry::VideoRegistration {
1006 node_id,
1007 source: w.source.as_str().to_string(),
1008 autoplay: w.autoplay,
1009 loop_playback: w.loop_playback,
1010 audio: w.audio.clone(),
1011 });
1012 Self {
1013 kind: Box::new(WidgetKind::Video(w)),
1014 }
1015 }
1016}
1017impl From<ZStack> for Widget {
1018 fn from(w: ZStack) -> Self {
1019 Self {
1020 kind: Box::new(WidgetKind::ZStack(w)),
1021 }
1022 }
1023}
1024impl From<Overlay> for Widget {
1025 fn from(w: Overlay) -> Self {
1026 Self {
1027 kind: Box::new(WidgetKind::Overlay(w)),
1028 }
1029 }
1030}
1031impl From<ContextMenuRegion> for Widget {
1032 fn from(w: ContextMenuRegion) -> Self {
1033 Self {
1034 kind: Box::new(WidgetKind::ContextMenuRegion(w)),
1035 }
1036 }
1037}
1038
1039impl From<Container> for Widget {
1040 fn from(w: Container) -> Self {
1041 Self {
1042 kind: Box::new(WidgetKind::Container(w)),
1043 }
1044 }
1045}
1046impl From<GestureDetector> for Widget {
1047 fn from(w: GestureDetector) -> Self {
1048 Self {
1049 kind: Box::new(WidgetKind::GestureDetector(w)),
1050 }
1051 }
1052}
1053impl From<Grid> for Widget {
1054 fn from(w: Grid) -> Self {
1055 Self {
1056 kind: Box::new(WidgetKind::Grid(w)),
1057 }
1058 }
1059}
1060impl From<GridItem> for Widget {
1061 fn from(w: GridItem) -> Self {
1062 Self {
1063 kind: Box::new(WidgetKind::GridItem(w)),
1064 }
1065 }
1066}
1067impl From<Responsive> for Widget {
1068 fn from(w: Responsive) -> Self {
1069 Self {
1070 kind: Box::new(WidgetKind::Responsive(w)),
1071 }
1072 }
1073}
1074impl From<Checkbox> for Widget {
1075 fn from(w: Checkbox) -> Self {
1076 Self {
1077 kind: Box::new(WidgetKind::Checkbox(w)),
1078 }
1079 }
1080}
1081impl From<Switch> for Widget {
1082 fn from(w: Switch) -> Self {
1083 Self {
1084 kind: Box::new(WidgetKind::Switch(w)),
1085 }
1086 }
1087}
1088impl From<Radio> for Widget {
1089 fn from(w: Radio) -> Self {
1090 Self {
1091 kind: Box::new(WidgetKind::Radio(w)),
1092 }
1093 }
1094}
1095impl From<SafeArea> for Widget {
1096 fn from(w: SafeArea) -> Self {
1097 Self {
1098 kind: Box::new(WidgetKind::SafeArea(w)),
1099 }
1100 }
1101}
1102impl From<Composite> for Widget {
1103 fn from(w: Composite) -> Self {
1104 Self {
1105 kind: Box::new(WidgetKind::Composite(w)),
1106 }
1107 }
1108}
1109impl From<Positioned> for Widget {
1110 fn from(w: Positioned) -> Self {
1111 Self {
1112 kind: Box::new(WidgetKind::Positioned(w)),
1113 }
1114 }
1115}
1116impl From<Spacer> for Widget {
1117 fn from(w: Spacer) -> Self {
1118 Self {
1119 kind: Box::new(WidgetKind::Spacer(w)),
1120 }
1121 }
1122}
1123impl From<Slider> for Widget {
1124 fn from(w: Slider) -> Self {
1125 Self {
1126 kind: Box::new(WidgetKind::Slider(w)),
1127 }
1128 }
1129}
1130impl From<LazyColumn> for Widget {
1131 fn from(w: LazyColumn) -> Self {
1132 Self {
1133 kind: Box::new(WidgetKind::LazyColumn(w)),
1134 }
1135 }
1136}
1137impl From<Icon> for Widget {
1138 fn from(w: Icon) -> Self {
1139 Self {
1140 kind: Box::new(WidgetKind::Icon(w)),
1141 }
1142 }
1143}
1144
1145#[derive(Clone, Debug, Serialize, Deserialize)]
1146pub struct InternalRenderNode {
1147 pub debug_tag: String,
1148 #[serde(skip)]
1149 pub lowerer: Option<Arc<dyn InternalLowerer>>,
1150 #[serde(skip)]
1154 pub render_object: Option<Arc<dyn CustomRenderObject>>,
1155}
1156
1157pub type CustomWidget = InternalRenderNode;
1158
1159impl From<CustomWidget> for Widget {
1160 fn from(node: CustomWidget) -> Self {
1161 Widget::custom(node)
1162 }
1163}
1164
1165#[cfg(test)]
1166mod visitor_tests {
1167 use super::*;
1168
1169 #[test]
1170 fn visitor_walks_nested_widgets_and_can_stop() {
1171 let root: Widget = Column {
1172 children: vec![Text::new("first").into(), Text::new("second").into()],
1173 ..Default::default()
1174 }
1175 .into();
1176 let mut visited = 0;
1177 let result = root.visit(&mut |widget| {
1178 visited += 1;
1179 if matches!(widget.kind(), WidgetKind::Text(_)) {
1180 ControlFlow::Break(())
1181 } else {
1182 ControlFlow::Continue(())
1183 }
1184 });
1185 assert!(matches!(result, ControlFlow::Break(())));
1186 assert_eq!(visited, 2);
1187 }
1188}