1use std::sync::Arc;
4
5use crate::callback::Callback;
6use crate::core::element::{Element, ElementKind};
7use crate::layout::axis::Axis;
8use crate::style::{
9 Align, BorderStyle, Justify, LayoutConstraints, Length, Padding, RichText, Style, StyleSlot,
10};
11use crate::widgets::TabsEvent;
12use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
13
14pub(crate) mod exit_retention;
15pub(crate) mod layout;
16pub(crate) mod node;
17pub(crate) mod reconcile;
18
19pub(crate) use self::layout::measure_stack;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Hash)]
23pub enum TabVariant {
24 #[default]
26 Classic,
27 Minimal,
29 Custom {
31 active_brackets: (char, char),
33 separator: &'static str,
35 },
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
40pub enum FocusSizing {
41 #[default]
43 None,
44 Accordion(FocusAccordion),
46}
47
48#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
50pub enum FocusScope {
51 #[default]
53 None,
54 Exclude,
57 Contain,
59}
60
61impl FocusSizing {
62 pub fn accordion() -> Self {
64 Self::Accordion(FocusAccordion::default())
65 }
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
70pub struct FocusAccordion {
71 pub focused_min: u16,
73 pub collapsed: u16,
75 pub tiny_collapsed: u16,
77 pub expanded_weight: u16,
79 pub squash_threshold: u16,
81 pub tiny_threshold: u16,
83 pub sticky: bool,
89}
90
91impl Default for FocusAccordion {
92 fn default() -> Self {
93 Self {
94 focused_min: 7,
95 collapsed: 3,
96 tiny_collapsed: 1,
97 expanded_weight: 2,
98 squash_threshold: 28,
99 tiny_threshold: 21,
100 sticky: true,
101 }
102 }
103}
104
105impl TabVariant {
106 pub(crate) fn separator(&self) -> &'static str {
107 match self {
108 Self::Classic => "|",
109 Self::Minimal => " - ",
110 Self::Custom { separator, .. } => separator,
111 }
112 }
113
114 pub(crate) fn separator_width(&self) -> usize {
115 UnicodeWidthStr::width(self.separator())
116 }
117
118 pub(crate) fn active_padding_width(&self) -> (usize, usize) {
119 match self {
120 Self::Classic => (2, 2),
121 Self::Minimal => (0, 0),
122 Self::Custom {
123 active_brackets: (l, r),
124 ..
125 } => (
126 UnicodeWidthChar::width(*l).unwrap_or(1),
127 UnicodeWidthChar::width(*r).unwrap_or(1),
128 ),
129 }
130 }
131
132 pub(crate) fn inactive_padding_width(&self) -> (usize, usize) {
133 match self {
134 Self::Classic => (1, 1),
135 Self::Minimal => (0, 0),
136 Self::Custom { .. } => (0, 0),
137 }
138 }
139
140 pub(crate) fn active_surround(
141 &self,
142 ) -> (
143 std::borrow::Cow<'static, str>,
144 std::borrow::Cow<'static, str>,
145 ) {
146 use std::borrow::Cow;
147 match self {
148 Self::Classic => (Cow::Borrowed("[ "), Cow::Borrowed(" ]")),
149 Self::Minimal => (Cow::Borrowed(""), Cow::Borrowed("")),
150 Self::Custom {
151 active_brackets: (l, r),
152 ..
153 } => (Cow::Owned(l.to_string()), Cow::Owned(r.to_string())),
154 }
155 }
156
157 pub(crate) fn inactive_surround(
158 &self,
159 ) -> (
160 std::borrow::Cow<'static, str>,
161 std::borrow::Cow<'static, str>,
162 ) {
163 use std::borrow::Cow;
164 match self {
165 Self::Classic => (Cow::Borrowed(" "), Cow::Borrowed(" ")),
166 Self::Minimal => (Cow::Borrowed(""), Cow::Borrowed("")),
167 Self::Custom { .. } => (Cow::Borrowed(""), Cow::Borrowed("")),
168 }
169 }
170}
171
172#[derive(Clone, Debug)]
174pub(crate) struct StackProps {
175 pub gap: u16,
177 pub padding: Padding,
180 pub style: Style,
182 pub align: Align,
185 pub justify: Justify,
188 pub width: Length,
191 pub height: Length,
194 pub focus_sizing: FocusSizing,
196 pub focus_scope: FocusScope,
198 pub border: bool,
200 pub border_style: BorderStyle,
203 pub even_flex: bool,
205}
206
207impl Default for StackProps {
208 fn default() -> Self {
209 Self {
210 gap: 0,
211 padding: Padding::default(),
212 style: Style::default(),
213 align: Align::Start,
214 justify: Justify::Start,
215 width: Length::Flex(1),
217 height: Length::Flex(1),
218 focus_sizing: FocusSizing::None,
219 focus_scope: FocusScope::None,
220 border: false,
221 border_style: BorderStyle::Plain,
222 even_flex: false,
223 }
224 }
225}
226
227macro_rules! impl_stack_props {
228 ($name:ident) => {
229 impl $name {
230 pub fn child(mut self, child: impl Into<Element>) -> Self {
232 self.children.push(child.into());
233 self
234 }
235
236 pub fn children(mut self, children: impl IntoIterator<Item = Element>) -> Self {
239 self.children = children.into_iter().collect();
240 self
241 }
242
243 pub fn border(mut self, border: bool) -> Self {
245 self.props.border = border;
246 self
247 }
248
249 pub fn border_style(mut self, border_style: BorderStyle) -> Self {
251 self.props.border_style = border_style;
252 self
253 }
254
255 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
257 self.props.padding = padding.into();
258 self
259 }
260
261 pub fn style(mut self, style: Style) -> Self {
263 self.props.style = style;
264 self
265 }
266
267 pub fn gap(mut self, gap: u16) -> Self {
269 self.props.gap = gap;
270 self
271 }
272
273 pub fn align(mut self, align: Align) -> Self {
275 self.props.align = align;
276 self
277 }
278
279 pub fn even_flex(mut self, even: bool) -> Self {
281 self.props.even_flex = even;
282 self
283 }
284
285 pub fn justify(mut self, justify: Justify) -> Self {
294 self.props.justify = justify;
295 self
296 }
297
298 pub fn width(mut self, width: Length) -> Self {
300 self.props.width = width;
301 self
302 }
303
304 pub fn height(mut self, height: Length) -> Self {
306 self.props.height = height;
307 self
308 }
309
310 pub fn focus_sizing(mut self, sizing: FocusSizing) -> Self {
312 self.props.focus_sizing = sizing;
313 self
314 }
315
316 pub fn focus_scope(mut self, scope: FocusScope) -> Self {
318 self.props.focus_scope = scope;
319 self
320 }
321 }
322 };
323}
324
325#[derive(Clone, Default)]
327pub struct VStack {
328 pub(crate) props: StackProps,
330 pub(crate) children: Vec<Element>,
332 pub(crate) tab_titles: Vec<RichText>,
334 pub(crate) active_tab: usize,
336 pub(crate) on_tab_change: Option<Callback<TabsEvent>>,
338 pub(crate) active_tab_style: StyleSlot,
340 pub(crate) inactive_tab_style: Style,
342 pub(crate) tab_variant: TabVariant,
344 pub(crate) title_prefix: Option<Arc<str>>,
346}
347
348impl_stack_props!(VStack);
349
350impl VStack {
351 pub fn new() -> Self {
353 Self::default()
354 }
355
356 pub fn tab_titles<I, S>(mut self, titles: I) -> Self
358 where
359 I: IntoIterator<Item = S>,
360 S: Into<RichText>,
361 {
362 self.tab_titles = titles.into_iter().map(Into::into).collect();
363 self
364 }
365
366 pub fn active_tab(mut self, active_tab: usize) -> Self {
368 self.active_tab = active_tab;
369 self
370 }
371
372 pub fn active_tab_style(mut self, style: Style) -> Self {
374 self.active_tab_style = StyleSlot::Replace(style);
375 self
376 }
377
378 pub fn extend_active_tab_style(mut self, style: Style) -> Self {
380 self.active_tab_style = StyleSlot::Extend(style);
381 self
382 }
383
384 pub fn inherit_active_tab_style(mut self) -> Self {
386 self.active_tab_style = StyleSlot::Inherit;
387 self
388 }
389
390 pub fn active_tab_style_slot(mut self, slot: StyleSlot) -> Self {
392 self.active_tab_style = slot;
393 self
394 }
395
396 pub fn inactive_tab_style(mut self, style: Style) -> Self {
398 self.inactive_tab_style = style;
399 self
400 }
401
402 pub fn on_tab_change(mut self, cb: Callback<TabsEvent>) -> Self {
404 self.on_tab_change = Some(cb);
405 self
406 }
407
408 pub fn tab_variant(mut self, variant: TabVariant) -> Self {
410 self.tab_variant = variant;
411 self
412 }
413
414 pub fn title_prefix(mut self, prefix: impl Into<Arc<str>>) -> Self {
416 self.title_prefix = Some(prefix.into());
417 self
418 }
419
420 pub(crate) fn border_index_at_col(
421 tab_titles: &[RichText],
422 active_tab: usize,
423 tab_variant: TabVariant,
424 col: usize,
425 ) -> Option<usize> {
426 let mut x = 0usize;
427
428 for (i, title) in tab_titles.iter().enumerate() {
429 let title_w = title.width();
430
431 let (pad_l, pad_r) = if i == active_tab {
432 tab_variant.active_padding_width()
433 } else {
434 tab_variant.inactive_padding_width()
435 };
436
437 let w = title_w.saturating_add(pad_l).saturating_add(pad_r);
438
439 if col < x.saturating_add(w) {
440 return Some(i);
441 }
442 x = x.saturating_add(w);
443
444 if i + 1 < tab_titles.len() {
446 let sep_w = tab_variant.separator_width();
447 if col < x.saturating_add(sep_w) {
448 return None; }
450 x = x.saturating_add(sep_w);
451 }
452 }
453
454 None
455 }
456}
457
458impl From<VStack> for Element {
459 fn from(value: VStack) -> Self {
460 let has_children = !value.children.is_empty();
461 let is_flex_h = matches!(value.props.height, Length::Flex(_));
464 let is_flex_w = matches!(value.props.width, Length::Flex(_));
465 let is_auto_h = matches!(value.props.height, Length::Auto);
466 let chrome_h = value.props.padding.vertical() + if value.props.border { 2 } else { 0 };
467 let auto_h_depends_on_width = is_auto_h
468 && value
469 .children
470 .iter()
471 .any(crate::widgets::scroll_child_height_depends_on_width);
472
473 let (min_w, measured_min_h) = match (is_flex_w, is_flex_h) {
474 (true, true) => {
475 let mut w = value.props.padding.horizontal();
476 let mut h = value.props.padding.vertical();
477 if value.props.border {
478 w += 2;
479 h += 2;
480 }
481 if has_children {
482 w = w.max(1);
483 h = h.max(1);
484 }
485 (w, h)
486 }
487 (true, false) => {
488 let (_w, h) =
489 measure_stack(&value.props, &value.children, Axis::Vertical, None, None);
490 let mut min_w = value.props.padding.horizontal();
491 if value.props.border {
492 min_w += 2;
493 }
494 (min_w, h)
495 }
496 (false, true) => {
497 let (w, content_h) =
498 measure_stack(&value.props, &value.children, Axis::Vertical, None, None);
499 let mut chrome_h = value.props.padding.vertical();
500 if value.props.border {
501 chrome_h += 2;
502 }
503 (w, chrome_h.max(content_h))
504 }
505 (false, false) => {
506 measure_stack(&value.props, &value.children, Axis::Vertical, None, None)
507 }
508 };
509
510 let min_h = if auto_h_depends_on_width {
511 chrome_h
512 } else {
513 measured_min_h
514 };
515
516 Element::new(ElementKind::VStack(value)).with_layout(
517 LayoutConstraints::default()
518 .min_width(Length::Px(min_w))
519 .min_height(Length::Px(min_h)),
520 )
521 }
522}
523
524impl crate::layout::hash::LayoutHash for VStack {
525 fn layout_hash(
526 &self,
527 hasher: &mut impl std::hash::Hasher,
528 recurse: &dyn Fn(&Element) -> Option<u64>,
529 ) -> Option<()> {
530 crate::layout::hash::hash_stack_props(&self.props, hasher);
531 crate::layout::hash::hash_children(&self.children, hasher, recurse)
532 }
533}
534
535#[derive(Clone)]
537pub struct HStack {
538 pub(crate) props: StackProps,
540 pub(crate) children: Vec<Element>,
542}
543
544impl Default for HStack {
545 fn default() -> Self {
546 Self {
547 props: StackProps {
548 align: Align::Center,
549 ..StackProps::default()
550 },
551 children: Vec::new(),
552 }
553 }
554}
555
556impl_stack_props!(HStack);
557
558impl HStack {
559 pub fn new() -> Self {
561 Self::default()
562 }
563}
564
565impl From<HStack> for Element {
566 fn from(value: HStack) -> Self {
567 let has_children = !value.children.is_empty();
568 let is_flex_h = matches!(value.props.height, Length::Flex(_));
571 let is_flex_w = matches!(value.props.width, Length::Flex(_));
572 let is_auto_h = matches!(value.props.height, Length::Auto);
573 let chrome_h = value.props.padding.vertical() + if value.props.border { 2 } else { 0 };
574 let auto_h_depends_on_width = is_auto_h
575 && value
576 .children
577 .iter()
578 .any(crate::widgets::scroll_child_height_depends_on_width);
579
580 let (min_w, measured_min_h) = match (is_flex_w, is_flex_h) {
581 (true, true) => {
582 let mut chrome_w = value.props.padding.horizontal();
586 let mut chrome_h = value.props.padding.vertical();
587 if value.props.border {
588 chrome_w += 2;
589 chrome_h += 2;
590 }
591 if has_children {
592 chrome_w = chrome_w.max(1);
593 chrome_h = chrome_h.max(1);
594 }
595 (chrome_w, chrome_h)
596 }
597 (true, false) => {
598 let (_w, h) =
599 measure_stack(&value.props, &value.children, Axis::Horizontal, None, None);
600 let mut min_w = value.props.padding.horizontal();
601 if value.props.border {
602 min_w += 2;
603 }
604 (min_w, h)
605 }
606 (false, true) => {
607 let (w, content_h) =
608 measure_stack(&value.props, &value.children, Axis::Horizontal, None, None);
609 let mut chrome_h = value.props.padding.vertical();
610 if value.props.border {
611 chrome_h += 2;
612 }
613 (w, chrome_h.max(content_h))
614 }
615 (false, false) => {
616 measure_stack(&value.props, &value.children, Axis::Horizontal, None, None)
617 }
618 };
619
620 let min_h = if auto_h_depends_on_width {
621 chrome_h
622 } else {
623 measured_min_h
624 };
625
626 Element::new(ElementKind::HStack(value)).with_layout(
627 LayoutConstraints::default()
628 .min_width(Length::Px(min_w))
629 .min_height(Length::Px(min_h)),
630 )
631 }
632}
633
634impl crate::layout::hash::LayoutHash for HStack {
635 fn layout_hash(
636 &self,
637 hasher: &mut impl std::hash::Hasher,
638 recurse: &dyn Fn(&Element) -> Option<u64>,
639 ) -> Option<()> {
640 crate::layout::hash::hash_stack_props(&self.props, hasher);
641 crate::layout::hash::hash_children(&self.children, hasher, recurse)
642 }
643}