teksilo_widgets/menu_bar/widget_impl.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! The [`Widget`] trait implementations for [`MenuBar`] and the two private
5//! wrappers it builds — `MenuOverlayHost` (the open dropdown's frame, which
6//! resets the bar's open index on dismissal and routes ArrowLeft / ArrowRight
7//! between sibling menus) and `RevealHeightBox` (which matches the floating
8//! collapsed bar's height to its hamburger).
9
10use super::*;
11
12// ---------------------------------------------------------------------------
13// MenuBar Widget impl
14// ---------------------------------------------------------------------------
15
16impl Widget for MenuBar {
17 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
18 // Mirror the model into the native OS menu bar (macOS) when requested.
19 // The bridge is a no-op without a `NativeMenuHandle` in app-state.
20 if self.native_mode.installs_native()
21 && cfg!(target_os = "macos")
22 && let Some(model) = &self.model
23 {
24 *self.native_binding.borrow_mut() = crate::menu::native::install(model, ctx);
25 }
26
27 // A runtime structural change (`MenuModel::push_item`/`remove`/…) bumps
28 // the model version; rebuild so the in-window dropdowns AND the native
29 // menu re-derive from the new structure.
30 if let Some(model) = &self.model {
31 model.version().bind_to(
32 ctx.self_id(),
33 ctx.binding_registry(),
34 teksilo_core::BindingLevel::Rebuild,
35 );
36 }
37
38 // On macOS with `Suppress`, the global menu bar IS the menu — render
39 // only the optional leading/trailing slots in-window (no triggers, no
40 // F10/Alt dispatcher).
41 if self.native_mode.suppresses_in_window() {
42 return self.build_suppressed(ctx);
43 }
44
45 let theme_signal = ctx.theme_signal();
46
47 let open_index: Signal<Option<usize>> = ctx.signal(None);
48 let menu_ctx = MenuContext::new(open_index);
49
50 // Build the full row: [leading_slot | triggers... | Spacer | trailing_slot]
51 let mut row = HStack::new().spacing(2.0);
52
53 // Leading slot (memoized — the same widgets survive each rebuild)
54 row = Self::add_slot(ctx, row, &mut self.leading_slot, &mut self.leading_slot_ids);
55
56 // Menu triggers + content
57 let mut trigger_ids = Vec::new();
58 let mut content_ids = Vec::new();
59 // Mnemonic table built alongside triggers: `lowercase char →
60 // trigger array index`. Drives the window-level dispatcher
61 // for Alt+letter activation.
62 let mut mnemonic_table: HashMap<char, usize> = HashMap::new();
63
64 // Both bar flavours re-derive their entries every build and re-run
65 // the (Fn) factories, so neither consumes the state it needs to
66 // rebuild: model-built bars re-derive from the (possibly mutated)
67 // model, classic `.menu()` bars iterate their retained entries by
68 // reference. Consuming `self.entries` here (the old `mem::take`)
69 // left the bar empty on the next theme / locale rebuild.
70 let model_entries = self.model.as_ref().map(Self::model_entries);
71 let entries: &[MenuBarEntry] = match &model_entries {
72 Some(derived) => derived,
73 None => &self.entries,
74 };
75 for (i, entry) in entries.iter().enumerate() {
76 let parsed: ParsedMnemonic = parse_mnemonic(&entry.label.resolve_now());
77
78 // Wrap factory output in MenuOverlayHost for focus/key handling
79 let host = MenuOverlayHost {
80 inner: Some((entry.factory)()),
81 menu_ctx: menu_ctx.clone(),
82 menu_index: i,
83 inner_id: None,
84 };
85 // Detached: a menu's content is shown through an overlay, never
86 // inline under the bar. Owned all the same, so a rebuilt menubar
87 // reaps the menus it replaced instead of stranding one host — and
88 // its whole `MenuList` — per rebuild.
89 // Built the first time *this* menu is opened. A menu bar used to
90 // build every menu's whole `MenuList` — and every submenu under it —
91 // on each rebuild of the bar, which a locale or shortcut change
92 // triggers. See `teksilo_core::deferred_subtree::DeferredSubtree`.
93 let opened_here = menu_ctx.open_index.map(move |open| *open == Some(i));
94 let content_id = ctx.add_detached_deferred(opened_here, host);
95 ctx.set_dormant(content_id);
96
97 let trigger = MenuBarTrigger {
98 label: entry.label.clone(),
99 stripped_name: parsed.stripped.clone(),
100 mnemonic_key: parsed.key_lower,
101 index: i,
102 menu_ctx: menu_ctx.clone(),
103 root_child_id: None,
104 };
105 let trigger_id = ctx.add(trigger);
106 row = row.child(trigger_id);
107
108 if let Some(k) = parsed.key_lower {
109 if let Some(prev) = mnemonic_table.insert(k, i) {
110 debug_assert!(
111 false,
112 "MenuBar: duplicate mnemonic {:?} (triggers {} and {})",
113 k, prev, i
114 );
115 }
116 }
117
118 trigger_ids.push(trigger_id);
119 content_ids.push(content_id);
120 }
121
122 // Register all trigger/content IDs in the context.
123 // focus_id is initially content_id; MenuOverlayHost::build() will
124 // overwrite it with the actual inner MenuList ID.
125 for (i, (&tid, &cid)) in trigger_ids.iter().zip(content_ids.iter()).enumerate() {
126 menu_ctx.register(i, tid, cid, cid);
127 }
128
129 // Spacer pushes triggers left, trailing slot right
130 row = row.child(Spacer::new());
131
132 // Trailing slot (memoized — the same widgets survive each rebuild)
133 row = Self::add_slot(
134 ctx,
135 row,
136 &mut self.trailing_slot,
137 &mut self.trailing_slot_ids,
138 );
139
140 let row_id = ctx.add(row);
141
142 let bg = RectWidget::new()
143 .background(SurfaceRole::Main)
144 .border_color(theme_signal.map(|t| t.colors.border.with_alpha(0.2)))
145 .border_width(0.0_f32);
146 let bg_id = ctx.add(bg);
147
148 let padding = Padding::symmetric(0.0, 2.0).child(row_id);
149 let padding_id = ctx.add(padding);
150
151 let zstack_id = ctx.add(ZStack::new().child(bg_id).child(padding_id));
152 // Shared cell holding the hamburger id once it's built below — the
153 // `RevealHeightBox` measures it to size the floating bar (filled at
154 // `anchor_cell.set(...)`, the same pattern as the overlay anchor).
155 let ham_cell: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
156 // In collapsible mode the `Role::MenuBar` landmark lives on the
157 // bar content node (not the composing widget) so it travels into
158 // the floating overlay AND so `overlay_is_host_surface` treats
159 // the revealed bar as a host (menu-open dismissal spares it). The
160 // content is wrapped in a `RevealHeightBox` so the *floating* bar's
161 // height matches the hamburger button — the triggers center
162 // vertically (the inner `HStack`'s default `VAlignment::Center`);
163 // the inline bar keeps its natural height. That, in turn, is
164 // wrapped in an `Unroll` so the floating bar unrolls out of the
165 // hamburger on open and rolls back into it on close (driven by
166 // `reveal_progress`; the overlay owns the tween + dismissal
167 // deferral — see the reveal closure below). `reveal_progress`
168 // stays at `1.0` for the inline bar, so `Unroll` is a no-op there.
169 let root_id = if self.collapse_policy.is_some() {
170 let height_box = ctx.add(RevealHeightBox {
171 child_id: None,
172 pending_child: Some(PendingChild::Id(zstack_id)),
173 revealed: self.revealed.clone(),
174 hamburger_id: ham_cell.clone(),
175 });
176 // Unrolls trailing-ward from the hamburger's edge (RTL flip is
177 // a follow-up, matching the docking handle-direction caveat).
178 ctx.add(
179 Unroll::from_progress(self.reveal_progress.clone())
180 .child(height_box)
181 .access_role(teksilo_core::accesskit::Role::MenuBar),
182 )
183 } else {
184 zstack_id
185 };
186 self.root_child_id = Some(root_id);
187 self.bar_id = Some(root_id);
188
189 // Collapsible (hamburger) mode: build the hamburger button and
190 // the reveal closure that floats the bar as an overlay; gate
191 // inline visibility on `collapsed` / `revealed`.
192 let mut children = vec![root_id];
193 let collapsible_reveal: Option<MenubarReveal> = if self.collapse_policy.is_some() {
194 let bar_id = root_id;
195 let revealed = self.revealed.clone();
196 let collapsed = self.collapsed.clone();
197 // Captured at build (EventContext can't reach motion / pref):
198 // the unroll tween duration and whether to snap. A theme /
199 // reduced-motion change rebuilds the bar, refreshing both.
200 let reveal_progress = self.reveal_progress.clone();
201 let reveal_duration = ctx.theme().motion.duration_collapse;
202 let reduced_motion = ctx.prefers_reduced_motion();
203
204 // The bar overlay trails the hamburger (the developer is
205 // responsible for placing the hamburger). The anchor cell is
206 // filled after the button is added, since the reveal closure
207 // is created before the button id is known.
208 let anchor_cell: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
209 // First trigger, focused on reveal so the bar is immediately
210 // keyboard-navigable (arrows move between menus, Enter opens).
211 let first_trigger = trigger_ids.first().copied();
212
213 let reveal: MenubarReveal = {
214 let revealed = revealed.clone();
215 let anchor_cell = anchor_cell.clone();
216 let reveal_progress = reveal_progress.clone();
217 Rc::new(move |ctx: &mut EventContext| {
218 if revealed.get() {
219 return; // idempotent — already revealed
220 }
221 revealed.set(true);
222 ctx.activate(bar_id);
223 let anchor = anchor_cell.get().unwrap_or(bar_id);
224 let on_dismiss: teksilo_core::overlay::OverlayDismissCallback = {
225 let revealed = revealed.clone();
226 Rc::new(move |_, _| revealed.set(false))
227 };
228 let request = OverlayRequest {
229 content_id: bar_id,
230 anchor,
231 placement: OverlayPlacement::TrailingEdge,
232 dismiss: DismissBehavior::EscapeOrClickOutside,
233 layer: OverlayLayer::InTree,
234 parent_overlay: None,
235 on_dismiss: Some(on_dismiss),
236 fade_duration: None,
237 };
238 if reduced_motion {
239 // No tween: show fully unrolled; dismissal is immediate.
240 reveal_progress.set(1.0);
241 ctx.show_overlay(request);
242 } else {
243 // Start rolled up, then the overlay tweens 0 → 1 on
244 // show and 1 → 0 on close (deferring teardown until
245 // the roll-back completes).
246 reveal_progress.set(0.0);
247 ctx.show_overlay_with_reveal(
248 request,
249 reveal_progress.clone(),
250 reveal_duration,
251 );
252 }
253 if let Some(trigger) = first_trigger {
254 ctx.request_focus(trigger);
255 }
256 })
257 };
258
259 // `IconButton::menu()` already advertises `HasPopup::Menu` and
260 // an accessible name ("Menu"). Binding `expanded_when(revealed)`
261 // completes the ARIA disclosure pattern: the button reports
262 // `expanded=true` while the bar is shown, `false` while collapsed.
263 let hamburger = IconButton::menu()
264 .size(self.hamburger_size)
265 .expanded_when(revealed.clone())
266 .on_activate_fn({
267 let reveal = reveal.clone();
268 move |ctx| reveal(ctx)
269 });
270 let hamburger_id = ctx.add(hamburger);
271 anchor_cell.set(Some(hamburger_id));
272 // Let the bar's `RevealHeightBox` measure the hamburger so the
273 // floating overlay's height matches the button.
274 ham_cell.set(Some(hamburger_id));
275 self.hamburger_id = Some(hamburger_id);
276
277 // Hamburger visible only while collapsed.
278 ctx.visible_when(hamburger_id, collapsed.clone());
279 // Bar active when shown inline (`!collapsed`) OR as the
280 // floating overlay (`revealed`). Keeping it active while
281 // revealed prevents the visibility binding from fighting the
282 // overlay activation.
283 let bar_active = collapsed.zip(&revealed).map(|(c, r)| !*c || *r);
284 ctx.visible_when(bar_id, bar_active);
285
286 children.push(hamburger_id);
287 Some(reveal)
288 } else {
289 None
290 };
291
292 // Window-level menubar key dispatcher (F10 / Alt+letter /
293 // Alt-tap). Installed on every platform — `MenuBar` is an
294 // in-window widget menu, not the OS system menu, so the
295 // dispatcher's job is to wire framework menus to keyboard
296 // accelerators regardless of host OS.
297 //
298 // **macOS**: the dispatcher's `Alt+letter` branch is compiled
299 // out (see `MenuBarDispatcher::try_handle`) because the OS
300 // rewrites Option+letter for accented character composition
301 // before the app sees the keystroke. F10 and bare-Alt-tap
302 // continue to fire on macOS through this same dispatcher.
303 //
304 // Drop the previous guard BEFORE installing the new one so
305 // the slot is empty when `install_menubar_dispatcher` runs
306 // its `debug_assert!(slot.is_none())`. Otherwise a rebuild
307 // of `MenuBar` (e.g. when a composing ancestor rebuilds)
308 // trips the assert in debug builds and would over-write the
309 // slot under another live guard in release.
310 if self.install_dispatcher
311 && let Some(window) = ctx.window()
312 {
313 *self.menubar_guard.borrow_mut() = None;
314 let inner = MenuBarDispatcher {
315 trigger_ids: trigger_ids.clone(),
316 mnemonic_table,
317 };
318 let dispatcher: Rc<dyn MenubarDispatcher> = match collapsible_reveal {
319 Some(reveal) => Rc::new(CollapsibleMenuBarDispatcher {
320 inner,
321 collapsed: self.collapsed.clone(),
322 reveal,
323 }),
324 None => Rc::new(inner),
325 };
326 let guard = window.install_menubar_dispatcher(dispatcher);
327 *self.menubar_guard.borrow_mut() = Some(guard);
328 }
329
330 children
331 }
332
333 fn layout_response(
334 &self,
335 proposal: SizeProposal,
336 ctx: &LayoutContext,
337 ) -> teksilo_core::widget::LayoutResponse {
338 // Collapsed: size to the hamburger's natural size (a small box),
339 // don't stretch to the full allotted width.
340 if self.collapse_policy.is_some() && self.collapsed.get() {
341 return match self.hamburger_id {
342 Some(id) => ctx
343 .child_size(id, SizeProposal::unspecified())
344 .unwrap_or_else(|| proposal.resolve(0.0, 0.0)),
345 None => proposal.resolve(0.0, 0.0),
346 }
347 .into();
348 }
349 match self.root_child_id {
350 Some(id) => {
351 let content_proposal = SizeProposal {
352 width: proposal.width,
353 height: None,
354 };
355 let size = ctx
356 .child_size(id, content_proposal)
357 .unwrap_or_else(|| proposal.resolve(0.0, 0.0));
358 Size::new(proposal.width.unwrap_or(size.width), size.height)
359 }
360 None => proposal.resolve(0.0, 0.0),
361 }
362 .into()
363 }
364
365 fn place_children(
366 &self,
367 bounds: Rect,
368 proposal: SizeProposal,
369 children: &mut [WidgetPlacement],
370 ctx: &LayoutContext,
371 ) {
372 // Responsive collapse decision (Toolbar pattern): compare the
373 // bar's intrinsic width against the allotted width and toggle
374 // `collapsed`, idempotently (the guard avoids relayout churn).
375 if let Some(policy) = self.collapse_policy {
376 let should_collapse = match policy {
377 CollapsePolicy::Always => true,
378 CollapsePolicy::Responsive => {
379 if self.revealed.get() {
380 // Don't un-collapse while the overlay is up — it
381 // would make the bar both inline and floating.
382 self.collapsed.get()
383 } else if let (Some(bar_id), Some(avail)) = (self.bar_id, proposal.width) {
384 ctx.measure_intrinsic(bar_id, SizeProposal::unspecified())
385 .map(|s| s.width)
386 .unwrap_or(0.0)
387 > avail + 0.5
388 } else {
389 // Unbounded width (or no bar) → never collapse.
390 false
391 }
392 }
393 };
394 if self.last_collapsed.get() != should_collapse {
395 self.last_collapsed.set(should_collapse);
396 self.collapsed.set(should_collapse);
397 }
398 }
399
400 // The hamburger keeps a constant width: place it at its intrinsic
401 // size, leading-aligned, so a stretching parent can't widen it.
402 // Everything else (the bar, inline or as the re-laid overlay) fills
403 // the bounds; dormant children are skipped by the layout pass.
404 let collapsed = self.collapse_policy.is_some() && self.collapsed.get();
405 for child in children.iter_mut() {
406 if collapsed && Some(child.id) == self.hamburger_id {
407 let size = ctx
408 .measure_intrinsic(child.id, SizeProposal::unspecified())
409 .unwrap_or_else(|| bounds.size());
410 let x = if ctx.is_rtl() {
411 bounds.right() - size.width
412 } else {
413 bounds.x
414 };
415 child.origin = Point::new(x, bounds.y);
416 child.size = size;
417 } else {
418 child.origin = bounds.origin();
419 child.size = bounds.size();
420 }
421 }
422 }
423
424 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
425 // In collapsible mode the `Role::MenuBar` landmark lives on the
426 // bar content node so it travels into the floating overlay; the
427 // composing widget node stays a generic container.
428 if self.collapse_policy.is_none() {
429 builder.set_role(teksilo_core::accesskit::Role::MenuBar);
430 }
431 }
432
433 fn children(&self) -> Vec<WidgetId> {
434 let mut v: Vec<WidgetId> = self.root_child_id.into_iter().collect();
435 if let Some(h) = self.hamburger_id {
436 v.push(h);
437 }
438 v
439 }
440
441 /// Reconcile on rebuild. The menu triggers are re-derived fresh each build
442 /// (the model may have changed) and the reconcile reaps the superseded
443 /// ones; the memoized leading/trailing slot widgets (see `add_slot`) are
444 /// re-attached by id and kept alive, so a stateful slot control — a search
445 /// field, a focused button, an avatar with hover state — survives a
446 /// model-version / theme / locale rebuild instead of being rebuilt from
447 /// scratch.
448 fn preserves_children_on_rebuild(&self) -> bool {
449 true
450 }
451}
452impl Widget for RevealHeightBox {
453 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
454 if let Some(pending) = self.pending_child.take() {
455 self.child_id = Some(match pending {
456 PendingChild::Id(id) => id,
457 PendingChild::Deferred(w) => ctx.add_boxed(w),
458 });
459 }
460 // Re-layout when the bar reveals / hides so the height switches
461 // between hamburger-matched (floating) and natural (inline).
462 self.revealed.bind_to(
463 ctx.self_id(),
464 ctx.binding_registry(),
465 teksilo_core::binding::BindingLevel::Relayout,
466 );
467 self.child_id.into_iter().collect()
468 }
469
470 fn layout_response(
471 &self,
472 proposal: SizeProposal,
473 ctx: &LayoutContext,
474 ) -> teksilo_core::widget::LayoutResponse {
475 let child = self.child_id;
476 if self.revealed.get() {
477 if let Some(ham) = self.hamburger_id.get() {
478 if let Some(h) = ctx
479 .measure_intrinsic(ham, SizeProposal::unspecified())
480 .map(|s| s.height)
481 {
482 let child_w = child
483 .and_then(|id| {
484 ctx.child_size(
485 id,
486 SizeProposal {
487 width: proposal.width,
488 height: Some(h),
489 },
490 )
491 })
492 .map(|s| s.width)
493 .unwrap_or(0.0);
494 let w = proposal.width.unwrap_or(child_w);
495 return Size::new(w, h).into();
496 }
497 }
498 }
499 child
500 .and_then(|id| ctx.child_size(id, proposal))
501 .unwrap_or(Size::ZERO)
502 .into()
503 }
504
505 fn place_children(
506 &self,
507 bounds: Rect,
508 _proposal: SizeProposal,
509 children: &mut [WidgetPlacement],
510 _ctx: &LayoutContext,
511 ) {
512 for child in children.iter_mut() {
513 child.origin = bounds.origin();
514 child.size = bounds.size();
515 }
516 }
517
518 fn children(&self) -> Vec<WidgetId> {
519 self.child_id.into_iter().collect()
520 }
521}
522impl Widget for MenuOverlayHost {
523 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
524 let inner_widget = self.inner.take().expect("MenuOverlayHost built twice");
525 let id = ctx.add_boxed(inner_widget);
526 self.inner_id = Some(id);
527
528 // Register inner widget as the focus target for this menu index
529 self.menu_ctx.set_focus_id(self.menu_index, id);
530
531 let menu_ctx = self.menu_ctx.clone();
532 let menu_index = self.menu_index;
533 let handler_set = HandlerSet::new()
534 .on_focus({
535 let menu_ctx = menu_ctx.clone();
536 move |gained: bool, _ctx: &mut EventContext| {
537 // Focus left this menu, so it is on its way out — record
538 // that, and nothing more. The dismissal itself belongs to
539 // the framework's focus-out rule
540 // (`dismiss_overlays_left_by_focus`), and the trigger gets
541 // its focus back from the overlay's own `focus_restore`.
542 //
543 // Doing either of those *here* was a race: this handler
544 // fires from inside the `FocusLost` dispatch, i.e. before
545 // `focus_with_origin_ops` has installed the new target, so
546 // the `request_focus(trigger)` it used to queue resolved
547 // first and was then silently overwritten by the very
548 // `set_focused` that was still in flight — a focus flash
549 // onto the trigger that no `FocusLost` ever accounted for.
550 // Keeping only the signal write leaves this side idempotent
551 // and lets every dismissal path (Escape, click-outside,
552 // Tab) converge on the same a11y state.
553 if !gained && menu_ctx.open_index.get() == Some(menu_index) {
554 menu_ctx.open_index.set(None);
555 }
556 }
557 })
558 .on_key({
559 let menu_ctx = menu_ctx.clone();
560 move |event: &WidgetEvent, ctx: &mut EventContext| -> EventResponse {
561 // These keys bubble up from the inner MenuList when it
562 // returns Ignored. Under RTL the bar is laid out
563 // right-to-left, so the previous/next arrows swap.
564 let (left_delta, right_delta) = if ctx.is_rtl() { (1, -1) } else { (-1, 1) };
565 match event {
566 WidgetEvent::KeyDown {
567 key: Key::ArrowLeft,
568 ..
569 } => {
570 menu_ctx.navigate(left_delta, ctx);
571 EventResponse::Handled
572 }
573 WidgetEvent::KeyDown {
574 key: Key::ArrowRight,
575 ..
576 } => {
577 menu_ctx.navigate(right_delta, ctx);
578 EventResponse::Handled
579 }
580 WidgetEvent::KeyDown {
581 key: Key::Escape, ..
582 } => {
583 menu_ctx.close(ctx);
584 EventResponse::Handled
585 }
586 _ => EventResponse::Ignored,
587 }
588 }
589 });
590 // NOT focusable — the inner MenuList receives focus directly.
591 // ArrowLeft/Right and FocusLost bubble from MenuList through here.
592 ctx.apply_self_handlers(handler_set);
593
594 vec![id]
595 }
596
597 fn layout_response(
598 &self,
599 proposal: SizeProposal,
600 ctx: &LayoutContext,
601 ) -> teksilo_core::widget::LayoutResponse {
602 self.inner_id
603 .and_then(|id| ctx.child_size(id, proposal))
604 .unwrap_or_else(|| proposal.resolve(0.0, 0.0))
605 .into()
606 }
607
608 fn place_children(
609 &self,
610 bounds: Rect,
611 _proposal: SizeProposal,
612 children: &mut [WidgetPlacement],
613 _ctx: &LayoutContext,
614 ) {
615 for child in children.iter_mut() {
616 child.origin = bounds.origin();
617 child.size = bounds.size();
618 }
619 }
620
621 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
622 // The inner widget (typically `MenuList`) owns the `Role::Menu`
623 // semantics. A second Menu role here would nest two Menu nodes
624 // per dropdown, confusing screen readers that look for a single
625 // Menu per popup. `GenericContainer` is the ARIA `none`/`presentation`
626 // equivalent: the host is kept in the tree for focus/key routing
627 // but is ignored by assistive tech.
628 builder.set_role(teksilo_core::accesskit::Role::GenericContainer);
629 }
630
631 fn children(&self) -> Vec<WidgetId> {
632 self.inner_id.into_iter().collect()
633 }
634}