1use std::cell::Cell;
2use std::rc::Rc;
3
4use ravenrs_core::{CursorIcon, Insets, LinearGradient, Point, Quad, Rect, Role, Scene, Size};
5use ravenrs_layout::{Align, Axis, Justify, LayoutStyle};
6use ravenrs_reactive::State;
7use ravenrs_style::{Semantic, TextStyle, Weight};
8
9use crate::context::{BuildCx, NodeId, Paint, QuadPaint, Semantics};
10use crate::overlay::{Entry, MenuList};
11use crate::{
12 AnyView, Button, ButtonSize, ButtonStyle, Divider, IconButton, IconName, Text, Tooltip, View, ViewSeq,
13 util,
14};
15
16macro_rules! stack {
17 ($(#[$doc:meta])* $name:ident, $axis:expr) => {
18 $(#[$doc])*
19 pub struct $name<C> {
20 children: C,
21 style: LayoutStyle,
22 }
23
24 #[cfg_attr(feature = "live", ravenrs_live::expose)]
25 impl<C: ViewSeq> $name<C> {
26 pub fn new(children: C) -> Self {
28 Self { children, style: LayoutStyle { spacing: 16.0, ..LayoutStyle::stack($axis) } }
29 }
30
31 pub fn spacing(mut self, spacing: f32) -> Self {
33 self.style.spacing = spacing;
34 self
35 }
36
37 pub fn padding(mut self, padding: impl Into<Insets>) -> Self {
39 self.style.padding = padding.into();
40 self
41 }
42
43 pub fn align(mut self, align: Align) -> Self {
45 self.style.align = align;
46 self
47 }
48
49 pub fn justify(mut self, justify: Justify) -> Self {
51 self.style.justify = justify;
52 self
53 }
54
55 pub fn width(mut self, width: f32) -> Self {
57 self.style.width = Some(width);
58 self
59 }
60
61 pub fn height(mut self, height: f32) -> Self {
63 self.style.height = Some(height);
64 self
65 }
66
67 pub fn max_width(mut self, width: f32) -> Self {
69 self.style.max_width = Some(width);
70 self
71 }
72
73 pub fn grow(mut self, grow: f32) -> Self {
77 self.style.grow = grow;
78 self.style.min_width.get_or_insert(0.0);
79 self.style.min_height.get_or_insert(0.0);
80 self
81 }
82
83 pub fn wrap(mut self) -> Self {
86 self.style.wrap = true;
87 self
88 }
89 }
90
91 impl<C> std::fmt::Debug for $name<C> {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct(stringify!($name)).field("style", &self.style).finish_non_exhaustive()
94 }
95 }
96
97 impl<C: ViewSeq> View for $name<C> {
98 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
99 let children = cx.children(&self.children);
100 cx.container(self.style, &children, Paint::None)
101 }
102 }
103 };
104}
105
106stack!(
107 VStack,
109 Axis::Vertical
110);
111stack!(
112 HStack,
114 Axis::Horizontal
115);
116
117#[derive(Debug)]
134pub struct ZStack<C> {
135 children: C,
136 align: Align,
137}
138
139#[cfg_attr(feature = "live", ravenrs_live::expose)]
140impl<C: ViewSeq> ZStack<C> {
141 pub fn new(children: C) -> Self {
143 Self { children, align: Align::Stretch }
144 }
145
146 pub fn align(mut self, align: Align) -> Self {
149 self.align = align;
150 self
151 }
152}
153
154struct ZLayer<'a> {
157 content: NodeId,
158 height: &'a Cell<f32>,
159}
160
161impl View for ZLayer<'_> {
162 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
163 let inner = cx.container(
164 LayoutStyle { align: Align::Stretch, ..LayoutStyle::stack(Axis::Vertical) },
165 &[self.content],
166 Paint::None,
167 );
168 cx.report_frame(inner);
169 self.height.set(cx.scroll_viewport().map_or(0.0, |size| size.height));
170 cx.container(
171 LayoutStyle { height: Some(0.0), align: Align::Stretch, ..LayoutStyle::stack(Axis::Vertical) },
172 &[inner],
173 Paint::None,
174 )
175 }
176}
177
178impl<C: ViewSeq> View for ZStack<C> {
179 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
180 let content = cx.children(&self.children);
181 let heights = vec![Cell::new(0.0_f32); content.len()];
182 let layers: Vec<NodeId> = content
183 .iter()
184 .zip(&heights)
185 .map(|(&content, height)| cx.child(&ZLayer { content, height }))
186 .collect();
187 let tallest = heights.iter().map(Cell::get).fold(0.0, f32::max);
188 let share = match self.align {
189 Align::Center => 0.5,
190 Align::End => 1.0,
191 _ => 0.0,
192 };
193 for (layer, height) in layers.iter().zip(&heights) {
194 cx.translate(*layer, Point::new(0.0, (tallest - height.get()) * share));
195 }
196 cx.container(
199 LayoutStyle {
200 height: Some(tallest),
201 align: self.align,
202 align_self: Some(Align::Stretch),
203 ..LayoutStyle::stack(Axis::Vertical)
204 },
205 &layers,
206 Paint::None,
207 )
208 }
209}
210
211#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
225pub struct PropertyRow {
226 label: String,
227 value: PropertyValue,
228}
229
230enum PropertyValue {
231 Text(String),
232 View(AnyView),
233}
234
235impl std::fmt::Debug for PropertyRow {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 f.debug_struct("PropertyRow").field("label", &self.label).finish_non_exhaustive()
238 }
239}
240
241#[cfg_attr(feature = "live", ravenrs_live::expose)]
242impl PropertyRow {
243 pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
245 Self { label: label.into(), value: PropertyValue::Text(value.into()) }
246 }
247
248 pub fn with(label: impl Into<String>, value: impl View + 'static) -> Self {
250 Self { label: label.into(), value: PropertyValue::View(Box::new(value)) }
251 }
252}
253
254impl View for PropertyRow {
255 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
256 let theme = *cx.theme();
257 let attrs = util::attrs(&theme, TextStyle::Callout, None);
258 let min_width = Some(cx.measure_text(&self.label, &attrs).width.ceil());
260 let label = cx.text(
261 &self.label,
262 &attrs,
263 theme.color(Semantic::InkMuted),
264 LayoutStyle { min_width, ..LayoutStyle::leaf() },
265 );
266 let (value, spoken) = match &self.value {
267 PropertyValue::Text(text) => {
268 let shown = if text.trim().is_empty() { "—" } else { text };
269 let node = cx.text_truncated(shown, &attrs, theme.color(Semantic::Ink), LayoutStyle::leaf());
270 cx.semantics(label, Semantics::hidden());
271 cx.semantics(node, Semantics::hidden());
272 (node, Some(shown.to_owned()))
273 }
274 PropertyValue::View(view) => (cx.child(view.as_ref()), None),
275 };
276 let node = cx.container(
277 LayoutStyle {
278 spacing: theme.spacing.s2,
279 justify: Justify::SpaceBetween,
280 align: Align::Center,
281 align_self: Some(Align::Stretch),
282 ..LayoutStyle::stack(Axis::Horizontal)
283 },
284 &[label, value],
285 Paint::None,
286 );
287 if let Some(spoken) = spoken {
288 cx.semantics(node, Semantics::new(Role::Label).name(self.label.clone()).value(spoken));
289 }
290 node
291 }
292}
293
294#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
305pub struct PageHeader {
306 title: String,
307 description: Option<String>,
308 badges: Vec<AnyView>,
309 actions: Vec<AnyView>,
310 compact: bool,
311}
312
313impl std::fmt::Debug for PageHeader {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 f.debug_struct("PageHeader").field("title", &self.title).finish_non_exhaustive()
316 }
317}
318
319#[cfg_attr(feature = "live", ravenrs_live::expose)]
320impl PageHeader {
321 pub fn new(title: impl Into<String>) -> Self {
323 Self {
324 title: title.into(),
325 description: None,
326 badges: Vec::new(),
327 actions: Vec::new(),
328 compact: false,
329 }
330 }
331
332 pub fn description(mut self, text: impl Into<String>) -> Self {
334 self.description = Some(text.into());
335 self
336 }
337
338 pub fn badge(mut self, badge: impl View + 'static) -> Self {
340 self.badges.push(Box::new(badge));
341 self
342 }
343
344 pub fn action(mut self, action: impl View + 'static) -> Self {
346 self.actions.push(Box::new(action));
347 self
348 }
349
350 pub fn compact(mut self) -> Self {
352 self.compact = true;
353 self
354 }
355}
356
357impl View for PageHeader {
358 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
359 let theme = *cx.theme();
360 let ink = theme.color(Semantic::Ink);
361 let (style, weight, level) = if self.compact {
362 (TextStyle::Headline, Some(Weight::SEMIBOLD), 2)
363 } else {
364 (TextStyle::Title, None, 1)
365 };
366 let title = util::label(cx, &self.title, style, weight, ink);
367 cx.semantics(title, Semantics::new(Role::Heading).level(level));
368 let description = self.description.as_ref().map(|text| {
369 let attrs = util::attrs(&theme, TextStyle::Callout, None);
370 cx.text_truncated(text, &attrs, theme.color(Semantic::InkMuted), LayoutStyle::leaf())
371 });
372 let mut top = vec![title];
373 if self.compact {
374 top.extend(description);
375 }
376 top.extend(self.badges.iter().map(|badge| cx.child(badge.as_ref())));
377 let top = cx.container(
378 LayoutStyle {
379 spacing: theme.spacing.s2,
380 align: Align::Center,
381 min_width: Some(0.0),
382 ..LayoutStyle::stack(Axis::Horizontal)
383 },
384 &top,
385 Paint::None,
386 );
387 let mut text = vec![top];
388 if !self.compact {
389 text.extend(description);
390 }
391 let text = cx.container(
392 LayoutStyle {
393 spacing: theme.spacing.s1,
394 grow: 1.0,
395 min_width: Some(0.0),
396 ..LayoutStyle::stack(Axis::Vertical)
397 },
398 &text,
399 Paint::None,
400 );
401 let mut children = vec![text];
402 if !self.actions.is_empty() {
403 let actions: Vec<NodeId> = self.actions.iter().map(|action| cx.child(action.as_ref())).collect();
404 children.push(cx.container(
405 LayoutStyle {
406 spacing: theme.spacing.s2,
407 align: Align::Center,
408 ..LayoutStyle::stack(Axis::Horizontal)
409 },
410 &actions,
411 Paint::None,
412 ));
413 }
414 cx.container(
415 LayoutStyle {
416 spacing: theme.spacing.s4,
417 align: Align::Center,
418 align_self: Some(Align::Stretch),
419 ..LayoutStyle::stack(Axis::Horizontal)
420 },
421 &children,
422 Paint::None,
423 )
424 }
425}
426
427#[derive(Clone, Copy, Debug, Default)]
429#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
430pub struct Spacer {
431 min: f32,
432}
433
434#[cfg_attr(feature = "live", ravenrs_live::expose)]
435impl Spacer {
436 pub fn new() -> Self {
438 Self::default()
439 }
440
441 pub fn min(mut self, min: f32) -> Self {
443 self.min = min;
444 self
445 }
446}
447
448impl View for Spacer {
449 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
450 let style = LayoutStyle {
451 grow: 1.0,
452 min_width: Some(self.min),
453 min_height: Some(self.min),
454 ..LayoutStyle::leaf()
455 };
456 cx.container(style, &[], Paint::None)
457 }
458}
459
460pub struct ScrollView<C> {
464 children: C,
465 style: LayoutStyle,
466 horizontal: bool,
467 scrollbar_outside: f32,
468}
469
470impl<C> std::fmt::Debug for ScrollView<C> {
471 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472 f.debug_struct("ScrollView").field("style", &self.style).finish_non_exhaustive()
473 }
474}
475
476#[cfg_attr(feature = "live", ravenrs_live::expose)]
477impl<C: ViewSeq> ScrollView<C> {
478 pub fn new(children: C) -> Self {
480 Self {
481 children,
482 style: LayoutStyle {
483 spacing: 16.0,
484 grow: 1.0,
485 align: Align::Stretch,
486 overflow_hidden: true,
487 min_height: Some(0.0),
488 ..LayoutStyle::stack(Axis::Vertical)
489 },
490 horizontal: false,
491 scrollbar_outside: 0.0,
492 }
493 }
494
495 #[must_use]
498 pub fn scrollbar_outside(mut self, by: f32) -> Self {
499 self.scrollbar_outside = by;
500 self
501 }
502
503 pub fn horizontal(mut self) -> Self {
506 self.horizontal = true;
507 self.style.align = Align::Start;
508 self.style.min_width = Some(0.0);
509 self
510 }
511
512 pub fn spacing(mut self, spacing: f32) -> Self {
514 self.style.spacing = spacing;
515 self
516 }
517
518 pub fn padding(mut self, padding: impl Into<Insets>) -> Self {
520 self.style.padding = padding.into();
521 self
522 }
523
524 pub fn height(mut self, height: f32) -> Self {
526 self.style.height = Some(height);
527 self.style.grow = 0.0;
528 self
529 }
530
531 pub fn align(mut self, align: Align) -> Self {
533 self.style.align = align;
534 self
535 }
536}
537
538impl<C: ViewSeq> View for ScrollView<C> {
539 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
540 let children = cx.children(&self.children);
541 let node = cx.container(self.style, &children, Paint::None);
542 cx.scrollable(node);
543 if self.scrollbar_outside != 0.0 {
544 cx.scrollbar_outside(node, self.scrollbar_outside);
545 }
546 if self.horizontal {
547 cx.scroll_horizontally(node);
548 }
549 node
550 }
551}
552
553const EDGE_ARROW_LENGTH: f32 = 40.0;
555const EDGE_ARROW_LEAST_STEP: f32 = 200.0;
557
558pub struct ScrollEdgeArrow<C> {
575 children: C,
576 vertical: bool,
577 spacing: f32,
578 padding: Insets,
579 height: Option<f32>,
580 fade: Semantic,
581}
582
583impl<C> std::fmt::Debug for ScrollEdgeArrow<C> {
584 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
585 f.debug_struct("ScrollEdgeArrow").field("vertical", &self.vertical).finish_non_exhaustive()
586 }
587}
588
589#[cfg_attr(feature = "live", ravenrs_live::expose)]
590impl<C: ViewSeq> ScrollEdgeArrow<C> {
591 pub fn new(children: C) -> Self {
593 Self {
594 children,
595 vertical: false,
596 spacing: 8.0,
597 padding: Insets::default(),
598 height: None,
599 fade: Semantic::Canvas,
600 }
601 }
602
603 pub fn vertical(mut self) -> Self {
605 self.vertical = true;
606 self
607 }
608
609 pub fn spacing(mut self, spacing: f32) -> Self {
611 self.spacing = spacing;
612 self
613 }
614
615 pub fn padding(mut self, padding: impl Into<Insets>) -> Self {
617 self.padding = padding.into();
618 self
619 }
620
621 pub fn height(mut self, height: f32) -> Self {
623 self.height = Some(height);
624 self
625 }
626
627 pub fn fade(mut self, fade: Semantic) -> Self {
630 self.fade = fade;
631 self
632 }
633}
634
635pub(crate) struct EdgeArrow {
637 pub forward: bool,
639 pub vertical: bool,
640 pub frame: Rect,
642 pub fade: Semantic,
643 pub label: String,
644 pub on_click: Rc<dyn Fn()>,
645}
646
647impl View for EdgeArrow {
648 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
649 let theme = *cx.theme();
650 let ink = theme.color(Semantic::Ink);
651 let color = if cx.is_hovered() { ink } else { ink.with_alpha(ink.a * 0.7) };
652 let icon_name = match (self.vertical, self.forward) {
653 (false, false) => IconName::ChevronLeft,
654 (false, true) => IconName::ChevronRight,
655 (true, false) => IconName::ChevronUp,
656 (true, true) => IconName::ChevronDown,
657 };
658 let icon = util::icon(cx, icon_name, 20.0, color);
659 let solid = if self.forward { 1.0 } else { 0.0 };
660 let (start, end) = if self.vertical {
661 (Point::new(0.5, solid), Point::new(0.5, 1.0 - solid))
662 } else {
663 (Point::new(solid, 0.5), Point::new(1.0 - solid, 0.5))
664 };
665 let from = theme.color(self.fade);
666 let node = cx.container(
667 LayoutStyle {
668 absolute: Some((self.frame.x(), self.frame.y())),
669 width: Some(self.frame.width()),
670 height: Some(self.frame.height()),
671 align: Align::Center,
672 justify: Justify::Center,
673 ..LayoutStyle::stack(Axis::Horizontal)
674 },
675 &[icon],
676 Paint::Quad(QuadPaint::new(LinearGradient { start, end, from, to: from.with_alpha(0.0) })),
677 );
678 cx.on_click(node, self.on_click.clone());
679 cx.semantics(node, Semantics::new(Role::Button).name(self.label.clone()));
680 cx.focusable(node, theme.radii.sm);
681 cx.cursor(node, CursorIcon::Pointer);
682 node
683 }
684}
685
686impl<C: ViewSeq> View for ScrollEdgeArrow<C> {
687 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
688 let labels = cx.labels();
689 let vertical = self.vertical;
690 let along = move |size: Size| if vertical { size.height } else { size.width };
691 let view = cx.scroll_viewport().unwrap_or(Size::ZERO);
692 let content = cx.measured_content().unwrap_or(Size::ZERO);
693 let last = (along(content) - along(view)).max(0.0);
694 let offset = cx.scroll_offset();
695
696 let wanted: State<Option<f32>> = cx.use_state(|| None);
698 let asked = wanted.get();
699 let here = asked.map_or(if vertical { offset.y } else { offset.x }, |to| to.clamp(0.0, last));
700 if asked.is_some() {
701 wanted.set(None);
702 cx.set_scroll_offset(if vertical {
703 Point::new(offset.x, here)
704 } else {
705 Point::new(here, offset.y)
706 });
707 }
708
709 let children = cx.children(&self.children);
710 let strip = cx.container(
711 LayoutStyle {
712 spacing: self.spacing,
713 padding: self.padding,
714 align: if vertical { Align::Stretch } else { Align::Center },
715 overflow_hidden: true,
716 grow: 1.0,
717 min_width: Some(0.0),
718 min_height: Some(0.0),
719 ..LayoutStyle::stack(if vertical { Axis::Vertical } else { Axis::Horizontal })
720 },
721 &children,
722 Paint::None,
723 );
724 cx.scrollable_virtual(strip, !vertical);
726 cx.measure_content(strip);
727
728 let step = (along(view) * 0.8).max(EDGE_ARROW_LEAST_STEP);
729 let arrow = |forward: bool| {
730 let at_end = along(view) - EDGE_ARROW_LENGTH;
731 let frame = match (vertical, forward) {
732 (false, false) => Rect::new(0.0, 0.0, EDGE_ARROW_LENGTH, view.height),
733 (false, true) => Rect::new(at_end, 0.0, EDGE_ARROW_LENGTH, view.height),
734 (true, false) => Rect::new(0.0, 0.0, view.width, EDGE_ARROW_LENGTH),
735 (true, true) => Rect::new(0.0, at_end, view.width, EDGE_ARROW_LENGTH),
736 };
737 let label = if forward { &labels.scroll_forward } else { &labels.scroll_back };
738 let wanted = wanted.clone();
739 let target = if forward { here + step } else { here - step };
740 EdgeArrow {
741 forward,
742 vertical,
743 frame,
744 fade: self.fade,
745 label: label.clone(),
746 on_click: Rc::new(move || wanted.set(Some(target))),
747 }
748 };
749 let show_back = here > 0.5;
750 let show_forward = here < last - 0.5;
751 let mut nodes = vec![strip];
752 let back = show_back.then(|| cx.keyed_child(1, &arrow(false)));
753 let forward = show_forward.then(|| cx.keyed_child(2, &arrow(true)));
754 nodes.extend(back);
755 nodes.extend(forward);
756 if asked.is_some() {
758 match (back, forward) {
759 (Some(other), None) | (None, Some(other)) => cx.request_focus_node(other),
760 _ => {}
761 }
762 }
763 cx.container(
764 LayoutStyle {
765 height: self.height,
766 grow: if vertical && self.height.is_none() { 1.0 } else { 0.0 },
767 min_width: Some(0.0),
768 min_height: Some(0.0),
769 align: Align::Stretch,
770 align_self: Some(Align::Stretch),
771 ..LayoutStyle::stack(Axis::Vertical)
772 },
773 &nodes,
774 Paint::None,
775 )
776 }
777}
778
779pub struct Grid<C> {
781 children: C,
782 style: LayoutStyle,
783}
784
785impl<C> std::fmt::Debug for Grid<C> {
786 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
787 f.debug_struct("Grid").field("style", &self.style).finish_non_exhaustive()
788 }
789}
790
791#[cfg_attr(feature = "live", ravenrs_live::expose)]
792impl<C: ViewSeq> Grid<C> {
793 pub fn new(columns: u16, children: C) -> Self {
795 Self {
796 children,
797 style: LayoutStyle {
798 spacing: 16.0,
799 grid_columns: Some(columns.max(1)),
800 align_self: Some(Align::Stretch),
801 ..LayoutStyle::stack(Axis::Vertical)
802 },
803 }
804 }
805
806 pub fn spacing(mut self, spacing: f32) -> Self {
808 self.style.spacing = spacing;
809 self
810 }
811}
812
813impl<C: ViewSeq> View for Grid<C> {
814 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
815 let children = cx.children(&self.children);
816 cx.container(self.style, &children, Paint::None)
817 }
818}
819
820const FOOTER_BREAKPOINT: f32 = 640.0;
822
823#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
840pub struct Footer {
841 left: Option<Box<dyn ViewSeq>>,
842 center: Option<Box<dyn ViewSeq>>,
843 right: Option<Box<dyn ViewSeq>>,
844 divider: bool,
845 breakpoint: f32,
846 stacked: bool,
847}
848
849impl std::fmt::Debug for Footer {
850 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
851 f.debug_struct("Footer")
852 .field("divider", &self.divider)
853 .field("breakpoint", &self.breakpoint)
854 .field("stacked", &self.stacked)
855 .finish_non_exhaustive()
856 }
857}
858
859impl Default for Footer {
860 fn default() -> Self {
861 Self {
862 left: None,
863 center: None,
864 right: None,
865 divider: true,
866 breakpoint: FOOTER_BREAKPOINT,
867 stacked: false,
868 }
869 }
870}
871
872#[cfg_attr(feature = "live", ravenrs_live::expose)]
873impl Footer {
874 pub fn new() -> Self {
876 Self::default()
877 }
878
879 pub fn text(text: impl Into<String>) -> Text {
881 Text::new(text).style(TextStyle::Caption).foreground(Semantic::InkMuted)
882 }
883
884 pub fn link(label: impl Into<String>) -> Button {
887 Button::new(label).style(ButtonStyle::Ghost).size(ButtonSize::Small)
888 }
889
890 pub fn left(mut self, items: impl ViewSeq + 'static) -> Self {
892 self.left = Some(Box::new(items));
893 self
894 }
895
896 pub fn center(mut self, items: impl ViewSeq + 'static) -> Self {
898 self.center = Some(Box::new(items));
899 self
900 }
901
902 pub fn right(mut self, items: impl ViewSeq + 'static) -> Self {
904 self.right = Some(Box::new(items));
905 self
906 }
907
908 pub fn divider(mut self, divider: bool) -> Self {
910 self.divider = divider;
911 self
912 }
913
914 pub fn breakpoint(mut self, width: f32) -> Self {
917 self.breakpoint = width;
918 self
919 }
920
921 pub fn stacked(mut self) -> Self {
924 self.stacked = true;
925 self
926 }
927
928 fn body(&self, cx: &mut BuildCx<'_>) -> NodeId {
929 let spacing = cx.theme().spacing;
930 let area = |items, justify| FooterArea { items, justify };
931 let (left, center, right) = (self.left.as_deref(), self.center.as_deref(), self.right.as_deref());
932 let center_only = left.is_none() && right.is_none();
933 if self.stacked || center_only || cx.viewport().width < self.breakpoint {
934 let areas: Vec<_> =
935 [left, center, right].into_iter().flatten().map(|a| area(Some(a), Justify::Center)).collect();
936 return cx.child(&VStack::new(areas).spacing(spacing.s2).align(Align::Stretch));
937 }
938 let (start, end) = (area(left, Justify::Start), area(right, Justify::End));
939 if center.is_some() {
940 cx.child(&Grid::new(3, (start, area(center, Justify::Center), end)).spacing(spacing.s4))
942 } else {
943 let sides = HStack::new((start, end))
944 .spacing(spacing.s4)
945 .align(Align::Center)
946 .justify(Justify::SpaceBetween);
947 cx.child(&sides.wrap())
948 }
949 }
950}
951
952impl View for Footer {
953 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
954 let spacing = cx.theme().spacing;
955 let body = self.body(cx);
956 let content = cx.container(
957 LayoutStyle {
958 padding: Insets::symmetric(spacing.s3, spacing.s4),
959 align: Align::Stretch,
960 ..LayoutStyle::stack(Axis::Vertical)
961 },
962 &[body],
963 Paint::None,
964 );
965 let divider = self.divider.then(|| cx.child(&Divider::horizontal()));
966 let children: Vec<NodeId> = divider.into_iter().chain([content]).collect();
967 let node = cx.container(
968 LayoutStyle {
969 align: Align::Stretch,
970 align_self: Some(Align::Stretch),
971 ..LayoutStyle::stack(Axis::Vertical)
972 },
973 &children,
974 Paint::None,
975 );
976 cx.semantics(node, Semantics::new(Role::ContentInfo));
977 node
978 }
979}
980
981struct FooterArea<'a> {
983 items: Option<&'a dyn ViewSeq>,
984 justify: Justify,
985}
986
987impl View for FooterArea<'_> {
988 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
989 let spacing = cx.theme().spacing.s3;
990 let children = self.items.map_or_else(Vec::new, |items| cx.children(items));
991 cx.container(
992 LayoutStyle {
993 spacing,
994 align: Align::Center,
995 justify: self.justify,
996 wrap: true,
997 min_width: Some(0.0),
999 ..LayoutStyle::stack(Axis::Horizontal)
1000 },
1001 &children,
1002 Paint::None,
1003 )
1004 }
1005}
1006
1007const TITLE_BAR_HEIGHT: f32 = 38.0;
1009
1010#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1030pub struct TitleBar {
1031 title: String,
1032 leading: Option<AnyView>,
1033 actions: Vec<AnyView>,
1034 on_minimize: Option<Rc<dyn Fn()>>,
1035 on_maximize: Option<Rc<dyn Fn()>>,
1036 on_close: Option<Rc<dyn Fn()>>,
1037 on_context_menu: Option<Rc<dyn Fn(Point)>>,
1038 maximized: bool,
1039 active: bool,
1040}
1041
1042impl std::fmt::Debug for TitleBar {
1043 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1044 f.debug_struct("TitleBar").field("title", &self.title).finish_non_exhaustive()
1045 }
1046}
1047
1048#[cfg_attr(feature = "live", ravenrs_live::expose)]
1049impl TitleBar {
1050 pub fn new(title: impl Into<String>) -> Self {
1052 Self {
1053 title: title.into(),
1054 leading: None,
1055 actions: Vec::new(),
1056 on_minimize: None,
1057 on_maximize: None,
1058 on_close: None,
1059 on_context_menu: None,
1060 maximized: false,
1061 active: true,
1062 }
1063 }
1064
1065 pub fn leading(mut self, view: impl View + 'static) -> Self {
1067 self.leading = Some(Box::new(view));
1068 self
1069 }
1070
1071 pub fn action(mut self, view: impl View + 'static) -> Self {
1073 self.actions.push(Box::new(view));
1074 self
1075 }
1076
1077 pub fn on_minimize(mut self, handler: impl Fn() + 'static) -> Self {
1079 self.on_minimize = Some(Rc::new(handler));
1080 self
1081 }
1082
1083 pub fn on_maximize(mut self, handler: impl Fn() + 'static) -> Self {
1085 self.on_maximize = Some(Rc::new(handler));
1086 self
1087 }
1088
1089 pub fn on_close(mut self, handler: impl Fn() + 'static) -> Self {
1091 self.on_close = Some(Rc::new(handler));
1092 self
1093 }
1094
1095 pub fn on_context_menu(mut self, handler: impl Fn(Point) + 'static) -> Self {
1097 self.on_context_menu = Some(Rc::new(handler));
1098 self
1099 }
1100
1101 pub fn maximized(mut self, maximized: bool) -> Self {
1103 self.maximized = maximized;
1104 self
1105 }
1106
1107 pub fn active(mut self, active: bool) -> Self {
1110 self.active = active;
1111 self
1112 }
1113}
1114
1115impl View for TitleBar {
1116 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1117 let theme = *cx.theme();
1118 let labels = cx.labels();
1119 let mut children = Vec::new();
1120 if let Some(leading) = &self.leading {
1121 children.push(cx.keyed_child(1, leading));
1122 }
1123 let ink = theme.color(if self.active { Semantic::Ink } else { Semantic::InkMuted });
1124 children.push(cx.text_truncated(
1125 &self.title,
1126 &util::attrs(&theme, TextStyle::Callout, Some(Weight::MEDIUM)),
1127 ink,
1128 LayoutStyle { grow: 1.0, min_width: Some(0.0), ..LayoutStyle::leaf() },
1129 ));
1130 for (i, action) in self.actions.iter().enumerate() {
1131 children.push(cx.keyed_child(10 + i as u64, action));
1132 }
1133 let (middle_icon, middle_label) = if self.maximized {
1134 (IconName::Copy, &labels.window_restore)
1135 } else {
1136 (IconName::Square, &labels.window_maximize)
1137 };
1138 let buttons = [
1139 (&self.on_minimize, IconName::Minus, &labels.window_minimize),
1140 (&self.on_maximize, middle_icon, middle_label),
1141 (&self.on_close, IconName::X, &labels.close),
1142 ];
1143 for (i, (handler, icon, label)) in buttons.into_iter().enumerate() {
1144 if let Some(handler) = handler.clone() {
1145 let button = Tooltip::new(
1146 IconButton::new(icon)
1147 .label(label.clone())
1148 .size(ButtonSize::Mini)
1149 .on_click(move || handler()),
1150 label.clone(),
1151 );
1152 children.push(cx.keyed_child(2 + i as u64, &button));
1153 }
1154 }
1155 let fill = theme.color(if self.active { Semantic::Surface } else { Semantic::Canvas });
1156 let line = theme.color(Semantic::Border);
1157 let node = cx.container(
1158 LayoutStyle {
1159 height: Some(TITLE_BAR_HEIGHT),
1160 padding: Insets::symmetric(0.0, theme.spacing.s3),
1161 spacing: theme.spacing.s2,
1162 align: Align::Center,
1163 align_self: Some(Align::Stretch),
1164 ..LayoutStyle::stack(Axis::Horizontal)
1165 },
1166 &children,
1167 Paint::Custom(Rc::new(move |f: Rect, scene: &mut Scene| {
1168 scene.push_quad(Quad::new(f, fill));
1169 scene.push_quad(Quad::new(Rect::new(f.x(), f.max_y() - 1.0, f.width(), 1.0), line));
1170 })),
1171 );
1172 if let Some(maximize) = &self.on_maximize {
1173 cx.on_double_click(node, maximize.clone());
1174 }
1175 if let Some(menu) = &self.on_context_menu {
1176 cx.on_context_menu(node, menu.clone());
1177 }
1178 cx.semantics(node, Semantics::new(Role::Group).name(self.title.clone()));
1179 node
1180 }
1181}
1182
1183#[derive(Default)]
1202#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1203pub struct StatusBar {
1204 left: Option<Box<dyn ViewSeq>>,
1205 center: Option<Box<dyn ViewSeq>>,
1206 right: Option<Box<dyn ViewSeq>>,
1207 center_tone: Option<crate::Tone>,
1208}
1209
1210impl std::fmt::Debug for StatusBar {
1211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1212 f.debug_struct("StatusBar").field("center_tone", &self.center_tone).finish_non_exhaustive()
1213 }
1214}
1215
1216#[cfg_attr(feature = "live", ravenrs_live::expose)]
1217impl StatusBar {
1218 pub const HEIGHT: f32 = 28.0;
1220
1221 pub fn new() -> Self {
1223 Self::default()
1224 }
1225
1226 pub fn text(text: impl Into<String>) -> Text {
1228 Text::new(text).style(TextStyle::Caption).foreground(Semantic::InkMuted)
1229 }
1230
1231 pub fn action(label: impl Into<String>) -> Button {
1234 Button::new(label).style(ButtonStyle::Ghost).size(ButtonSize::Mini)
1235 }
1236
1237 pub fn left(mut self, items: impl ViewSeq + 'static) -> Self {
1239 self.left = Some(Box::new(items));
1240 self
1241 }
1242
1243 pub fn center(mut self, items: impl ViewSeq + 'static) -> Self {
1246 self.center = Some(Box::new(items));
1247 self
1248 }
1249
1250 pub fn right(mut self, items: impl ViewSeq + 'static) -> Self {
1252 self.right = Some(Box::new(items));
1253 self
1254 }
1255
1256 pub fn center_tone(mut self, tone: crate::Tone) -> Self {
1258 self.center_tone = Some(tone);
1259 self
1260 }
1261
1262 fn side(cx: &mut BuildCx<'_>, items: Option<&dyn ViewSeq>) -> NodeId {
1263 let spacing = cx.theme().spacing.s1;
1264 let children = items.map_or_else(Vec::new, |items| cx.children(items));
1265 cx.container(
1266 LayoutStyle { spacing, align: Align::Center, ..LayoutStyle::stack(Axis::Horizontal) },
1267 &children,
1268 Paint::None,
1269 )
1270 }
1271}
1272
1273impl View for StatusBar {
1274 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1275 let theme = *cx.theme();
1276 let left = Self::side(cx, self.left.as_deref());
1277 let right = Self::side(cx, self.right.as_deref());
1278 let items = self.center.as_deref().map_or_else(Vec::new, |items| cx.children(items));
1279 let has_center = !items.is_empty();
1280 let fill = self.center_tone.filter(|_| has_center).map(|tone| tone.color(&theme).faded(0.16));
1281 let center = cx.container(
1282 LayoutStyle {
1283 spacing: theme.spacing.s1,
1284 padding: Insets::symmetric(0.0, theme.spacing.s2),
1285 align: Align::Center,
1286 align_self: Some(Align::Stretch),
1287 grow: 1.0,
1288 min_width: Some(0.0),
1289 overflow_hidden: true,
1290 ..LayoutStyle::stack(Axis::Horizontal)
1291 },
1292 &items,
1293 fill.map_or(Paint::None, |fill| Paint::Quad(QuadPaint::new(fill))),
1294 );
1295 if has_center {
1296 cx.semantics(center, Semantics::new(Role::Status));
1297 }
1298 let line = cx.container(
1299 LayoutStyle {
1300 spacing: theme.spacing.s2,
1301 padding: Insets::symmetric(0.0, theme.spacing.s2),
1302 align: Align::Center,
1303 height: Some(Self::HEIGHT),
1304 align_self: Some(Align::Stretch),
1305 ..LayoutStyle::stack(Axis::Horizontal)
1306 },
1307 &[left, center, right],
1308 Paint::None,
1309 );
1310 let divider = cx.child(&Divider::horizontal());
1311 let node = cx.container(
1312 LayoutStyle {
1313 align: Align::Stretch,
1314 align_self: Some(Align::Stretch),
1315 ..LayoutStyle::stack(Axis::Vertical)
1316 },
1317 &[divider, line],
1318 Paint::None,
1319 );
1320 cx.semantics(node, Semantics::new(Role::ContentInfo));
1321 node
1322 }
1323}
1324
1325#[derive(Clone, Debug)]
1327#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1328pub struct ToolRailItem {
1329 tools: Vec<RailToolInfo>,
1330}
1331
1332#[derive(Clone, Debug)]
1333struct RailToolInfo {
1334 icon: crate::IconName,
1335 name: String,
1336 shortcut: Option<String>,
1337}
1338
1339#[cfg_attr(feature = "live", ravenrs_live::expose)]
1340impl ToolRailItem {
1341 pub fn new(icon: crate::IconName, name: impl Into<String>) -> Self {
1343 Self { tools: vec![RailToolInfo { icon, name: name.into(), shortcut: None }] }
1344 }
1345
1346 pub fn shortcut(mut self, keys: impl Into<String>) -> Self {
1348 if let Some(tool) = self.tools.last_mut() {
1349 tool.shortcut = Some(keys.into());
1350 }
1351 self
1352 }
1353
1354 pub fn variant(mut self, icon: crate::IconName, name: impl Into<String>) -> Self {
1358 self.tools.push(RailToolInfo { icon, name: name.into(), shortcut: None });
1359 self
1360 }
1361}
1362
1363#[derive(Clone, Debug)]
1364enum RailEntry {
1365 Tool(ToolRailItem),
1366 Separator,
1367}
1368
1369const RAIL_HOLD: f64 = 0.4;
1371const RAIL_ICON_BUTTON: f32 = 36.0;
1373
1374#[derive(Debug)]
1398#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1399pub struct ToolRail {
1400 selected: ravenrs_reactive::Binding<usize>,
1401 entries: Vec<RailEntry>,
1402 width: Option<f32>,
1403 columns: usize,
1404 icons_only: bool,
1405}
1406
1407#[cfg_attr(feature = "live", ravenrs_live::expose)]
1408impl ToolRail {
1409 pub fn new<S: Into<String>>(
1411 selected: ravenrs_reactive::Binding<usize>,
1412 tools: impl IntoIterator<Item = (crate::IconName, S)>,
1413 ) -> Self {
1414 let entries =
1415 tools.into_iter().map(|(icon, name)| RailEntry::Tool(ToolRailItem::new(icon, name))).collect();
1416 Self { selected, entries, width: None, columns: 1, icons_only: false }
1417 }
1418
1419 pub fn tool(mut self, item: ToolRailItem) -> Self {
1421 self.entries.push(RailEntry::Tool(item));
1422 self
1423 }
1424
1425 pub fn separator(mut self) -> Self {
1427 self.entries.push(RailEntry::Separator);
1428 self
1429 }
1430
1431 pub fn width(mut self, width: f32) -> Self {
1433 self.width = Some(width);
1434 self
1435 }
1436
1437 pub fn icons_only(mut self, icons_only: bool) -> Self {
1439 self.icons_only = icons_only;
1440 self
1441 }
1442
1443 pub fn columns(mut self, columns: usize) -> Self {
1446 self.columns = columns.max(1);
1447 self
1448 }
1449}
1450
1451struct RailTool<'a> {
1452 tools: &'a [RailToolInfo],
1453 first: usize,
1455 selected: ravenrs_reactive::Binding<usize>,
1456 icons_only: bool,
1457}
1458
1459#[cfg_attr(feature = "live", ravenrs_live::expose)]
1460impl RailTool<'_> {
1461 fn variants(&self, shown: usize, last_used: &ravenrs_reactive::State<usize>) -> Vec<Entry> {
1463 self.tools
1464 .iter()
1465 .enumerate()
1466 .map(|(i, tool)| {
1467 let (selected, last_used, tool_at) =
1468 (self.selected.clone(), last_used.clone(), self.first + i);
1469 let mut item = crate::MenuItem::new(tool.name.clone())
1470 .icon(tool.icon)
1471 .checked(i == shown)
1472 .on_select(move || {
1473 last_used.set(i);
1474 selected.set(tool_at);
1475 });
1476 if let Some(keys) = &tool.shortcut {
1477 item = item.shortcut(keys.clone());
1478 }
1479 Entry::Item(item)
1480 })
1481 .collect()
1482 }
1483}
1484
1485impl View for RailTool<'_> {
1486 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1487 let theme = *cx.theme();
1488 let hovered = cx.is_hovered();
1489 let last_used = cx.use_state(|| 0_usize);
1490 let current = self.selected.get();
1491 let ours = current.checked_sub(self.first).filter(|i| *i < self.tools.len());
1492 let shown = ours.unwrap_or_else(|| (*last_used.peek()).min(self.tools.len() - 1));
1494 let tool = &self.tools[shown];
1495 let has_variants = self.tools.len() > 1;
1496 let is_selected = ours.is_some();
1497 let ink = theme.color(if is_selected { Semantic::Ink } else { Semantic::InkMuted });
1498 let mut parts = vec![crate::util::icon(cx, tool.icon, 18.0, ink)];
1499 if !self.icons_only {
1500 parts.push(crate::util::label(cx, &tool.name, TextStyle::Caption, None, ink));
1501 }
1502 if has_variants {
1503 parts.push(cx.container(
1505 LayoutStyle {
1506 width: Some(4.0),
1507 height: Some(4.0),
1508 absolute: Some((
1509 if self.icons_only { RAIL_ICON_BUTTON - 8.0 } else { 6.0 },
1510 if self.icons_only { RAIL_ICON_BUTTON - 8.0 } else { 6.0 },
1511 )),
1512 ..LayoutStyle::leaf()
1513 },
1514 &[],
1515 Paint::Quad(QuadPaint { corner_radius: 2.0, ..QuadPaint::new(ink) }),
1516 ));
1517 }
1518 let fill = if is_selected {
1519 theme.color(Semantic::Accent).faded(0.2)
1520 } else if hovered {
1521 theme.color(Semantic::Ink).faded(0.05)
1522 } else {
1523 ravenrs_core::Color::TRANSPARENT
1524 };
1525 let style = if self.icons_only {
1526 LayoutStyle {
1527 width: Some(RAIL_ICON_BUTTON),
1528 height: Some(RAIL_ICON_BUTTON),
1529 align: Align::Center,
1530 justify: Justify::Center,
1531 ..LayoutStyle::stack(Axis::Vertical)
1532 }
1533 } else {
1534 LayoutStyle {
1535 height: Some(56.0),
1536 spacing: theme.spacing.s1,
1537 align: Align::Center,
1538 justify: Justify::Center,
1539 align_self: Some(Align::Stretch),
1540 ..LayoutStyle::stack(Axis::Vertical)
1541 }
1542 };
1543 let node = cx.container(
1544 style,
1545 &parts,
1546 Paint::Quad(QuadPaint { corner_radius: theme.radii.md, ..QuadPaint::new(fill) }),
1547 );
1548 let open: ravenrs_reactive::State<bool> = cx.use_state(|| false);
1549 let is_open = has_variants && open.get();
1550 let mut semantics = Semantics::new(Role::Button).name(tool.name.clone()).selected(is_selected);
1551 if has_variants {
1552 semantics = semantics.expanded(is_open);
1553 }
1554 cx.semantics(node, semantics);
1555 let held = cx.use_state(|| Rc::new(Cell::new(None::<f64>))).get();
1557 let opened_by_hold = cx.use_state(|| Rc::new(Cell::new(false))).get();
1558 if has_variants {
1559 if cx.is_pressed() && !is_open {
1560 let now = cx.now();
1561 let due = held.get().unwrap_or(now + RAIL_HOLD);
1562 held.set(Some(due));
1563 if now >= due {
1564 opened_by_hold.set(true);
1565 open.set(true);
1566 } else {
1567 cx.wake_at(due);
1568 }
1569 } else {
1570 held.set(None);
1571 }
1572 }
1573 let (selected, tool_at, swallow) =
1574 (self.selected.clone(), self.first + shown, opened_by_hold.clone());
1575 cx.on_click(
1576 node,
1577 Rc::new(move || {
1578 if !swallow.replace(false) {
1579 selected.set(tool_at);
1580 }
1581 }),
1582 );
1583 cx.focusable(node, theme.radii.md);
1584 cx.cursor(node, ravenrs_core::CursorIcon::Pointer);
1585 if !has_variants {
1586 return node;
1587 }
1588 let at_right = open.clone();
1589 cx.on_key(
1590 node,
1591 Rc::new(move |key, _| {
1592 let opens = *key == ravenrs_core::Key::ArrowRight;
1593 if opens {
1594 at_right.set(true);
1595 }
1596 opens
1597 }),
1598 );
1599 let on_menu = open.clone();
1600 cx.on_context_menu(node, Rc::new(move |_| on_menu.set(true)));
1601 let was_open = cx.use_state(|| Rc::new(Cell::new(false))).get();
1602 if is_open {
1603 was_open.set(true);
1604 let close: Rc<dyn Fn()> = Rc::new(move || open.set(false));
1605 let entries = self.variants(shown, &last_used);
1606 cx.overlay(
1607 node,
1608 crate::Placement::Right,
1609 &MenuList { entries: &entries, close: close.clone(), on_side: None },
1610 Some(close),
1611 );
1612 } else if was_open.replace(false) {
1613 cx.request_focus_node(node);
1614 }
1615 node
1616 }
1617}
1618
1619impl View for ToolRail {
1620 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1621 let theme = *cx.theme();
1622 let icons_only = self.icons_only || self.columns > 1;
1623 let gap = 2.0;
1624 let padding = theme.spacing.s1;
1625 let columns = self.columns as f32;
1626 let width = self.width.unwrap_or(if icons_only {
1627 columns * RAIL_ICON_BUTTON + (columns - 1.0) * gap + padding * 2.0
1628 } else {
1629 76.0
1630 });
1631 let mut children = Vec::with_capacity(self.entries.len());
1632 let mut row = Vec::with_capacity(self.columns);
1633 let mut first = 0;
1634 let flush = |cx: &mut BuildCx<'_>, row: &mut Vec<NodeId>, children: &mut Vec<NodeId>| {
1635 if !row.is_empty() {
1636 children.push(cx.container(
1637 LayoutStyle { spacing: gap, ..LayoutStyle::stack(Axis::Horizontal) },
1638 row,
1639 Paint::None,
1640 ));
1641 row.clear();
1642 }
1643 };
1644 for entry in &self.entries {
1645 match entry {
1646 RailEntry::Tool(item) => {
1647 let place =
1648 RailTool { tools: &item.tools, first, selected: self.selected.clone(), icons_only };
1649 let name = item.tools.first().map_or("", |tool| tool.name.as_str());
1650 let node = if icons_only {
1651 cx.keyed_child(first as u64, &crate::Tooltip::new(place, name.to_owned()))
1652 } else {
1653 cx.keyed_child(first as u64, &place)
1654 };
1655 first += item.tools.len();
1656 if self.columns > 1 {
1657 row.push(node);
1658 if row.len() == self.columns {
1659 flush(cx, &mut row, &mut children);
1660 }
1661 } else {
1662 children.push(node);
1663 }
1664 }
1665 RailEntry::Separator => {
1666 flush(cx, &mut row, &mut children);
1667 children.push(cx.child(&Divider::horizontal()));
1668 }
1669 }
1670 }
1671 flush(cx, &mut row, &mut children);
1672 cx.container(
1673 LayoutStyle {
1674 width: Some(width),
1675 spacing: gap,
1676 padding: Insets::uniform(padding),
1677 align: Align::Stretch,
1678 ..LayoutStyle::stack(Axis::Vertical)
1679 },
1680 &children,
1681 Paint::None,
1682 )
1683 }
1684}