1pub mod box_metrics;
4pub mod layout;
5pub mod node;
6pub mod reconcile;
7
8pub(crate) use self::box_metrics::{FrameGeometry, FrameJoinOverlap, compute_frame_geometry};
9pub(crate) use self::layout::{measure_frame, measure_frame_chrome};
10pub(crate) use self::reconcile::reconcile_frame;
11
12use crate::callback::Callback;
13use crate::core::element::{Element, ElementKind};
14use crate::style::{
15 Align, BorderEdges, BorderStyle, Edge, LayoutConstraints, Length, Padding, RichText, Style,
16 StyleSlot,
17};
18use crate::widgets::{TabVariant, TabsEvent};
19
20pub use self::node::FrameNode;
21pub(crate) use self::node::FrameProps;
23
24#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
26pub struct FrameLabel {
27 pub content: RichText,
29 pub style: Option<Style>,
31 pub focused_style: Option<Style>,
33}
34
35impl FrameLabel {
36 pub fn new(content: impl Into<RichText>) -> Self {
38 Self {
39 content: content.into(),
40 style: None,
41 focused_style: None,
42 }
43 }
44
45 pub fn style(mut self, style: Style) -> Self {
47 self.style = Some(style);
48 self
49 }
50
51 pub fn focused_style(mut self, style: Style) -> Self {
53 self.focused_style = Some(style);
54 self
55 }
56}
57
58impl From<RichText> for FrameLabel {
59 fn from(content: RichText) -> Self {
60 Self::new(content)
61 }
62}
63
64impl From<String> for FrameLabel {
65 fn from(content: String) -> Self {
66 Self::new(content)
67 }
68}
69
70impl From<std::sync::Arc<str>> for FrameLabel {
71 fn from(content: std::sync::Arc<str>) -> Self {
72 Self::new(content)
73 }
74}
75
76impl From<&str> for FrameLabel {
77 fn from(content: &str) -> Self {
78 Self::new(content.to_owned())
79 }
80}
81
82impl From<crate::style::Span> for FrameLabel {
83 fn from(content: crate::style::Span) -> Self {
84 Self::new(content)
85 }
86}
87
88#[derive(Clone, Debug, Default, PartialEq, Eq)]
90pub struct BorderLabels {
91 pub left: Option<FrameLabel>,
93 pub center: Option<FrameLabel>,
95 pub right: Option<FrameLabel>,
97 pub style: Style,
99 pub focused_style: Option<Style>,
101 pub padding: Padding,
103}
104
105impl BorderLabels {
106 pub fn new() -> Self {
108 Self::default()
109 }
110
111 pub fn left(mut self, label: impl Into<FrameLabel>) -> Self {
113 self.left = Some(label.into());
114 self
115 }
116
117 pub fn center(mut self, label: impl Into<FrameLabel>) -> Self {
119 self.center = Some(label.into());
120 self
121 }
122
123 pub fn right(mut self, label: impl Into<FrameLabel>) -> Self {
125 self.right = Some(label.into());
126 self
127 }
128
129 pub fn style(mut self, style: Style) -> Self {
131 self.style = style;
132 self
133 }
134
135 pub fn focused_style(mut self, style: Style) -> Self {
137 self.focused_style = Some(style);
138 self
139 }
140
141 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
143 self.padding = padding.into();
144 self
145 }
146
147 pub(crate) fn has_labels(&self) -> bool {
148 [&self.left, &self.center, &self.right]
149 .into_iter()
150 .flatten()
151 .any(|label| !label.content.is_empty())
152 }
153
154 pub(crate) fn min_width(&self) -> usize {
155 [&self.left, &self.center, &self.right]
156 .into_iter()
157 .flatten()
158 .map(|label| {
159 label
160 .content
161 .width()
162 .saturating_add(self.padding.left as usize)
163 .saturating_add(self.padding.right as usize)
164 })
165 .sum()
166 }
167}
168
169#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
171pub enum BorderMergeMode {
172 Replace,
174 #[default]
176 Exact,
177 Fuzzy,
179}
180
181#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
183pub enum DecorationPlacement {
184 #[default]
186 Border,
187 Inside,
189 Outside,
191}
192
193#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
195pub enum DecorationGlyph {
196 AutoThin,
198 #[default]
200 AutoHeavy,
201 AutoDouble,
203 AutoBlock,
205 HorizontalThin,
207 HorizontalHeavy,
209 HorizontalDouble,
211 HorizontalBlock,
213 VerticalThin,
215 VerticalHeavy,
217 VerticalDouble,
219 HalfBlock,
221 HalfBlockTop,
223 HalfBlockBottom,
225 HalfBlockLeft,
227 HalfBlockRight,
229 CapTop,
231 CapBottom,
233 CapLeft,
235 CapRight,
237 CapTopHeavy,
239 CapBottomHeavy,
241 CapLeftHeavy,
243 CapRightHeavy,
245 Custom(char),
247}
248
249impl DecorationGlyph {
250 pub(crate) fn resolve(self, edge: Edge) -> char {
251 match self {
252 Self::AutoThin => match edge {
253 Edge::Left | Edge::Right => '│',
254 Edge::Top | Edge::Bottom => '─',
255 },
256 Self::AutoHeavy => match edge {
257 Edge::Left | Edge::Right => '┃',
258 Edge::Top | Edge::Bottom => '━',
259 },
260 Self::AutoDouble => match edge {
261 Edge::Left | Edge::Right => '║',
262 Edge::Top | Edge::Bottom => '═',
263 },
264 Self::AutoBlock => match edge {
265 Edge::Left | Edge::Right => '┃',
266 Edge::Top | Edge::Bottom => '▬',
267 },
268 Self::HorizontalThin => '─',
269 Self::HorizontalHeavy => '━',
270 Self::HorizontalDouble => '═',
271 Self::HorizontalBlock => '▬',
272 Self::VerticalThin => '│',
273 Self::VerticalHeavy => '┃',
274 Self::VerticalDouble => '║',
275 Self::HalfBlock => match edge {
276 Edge::Top => '▄',
277 Edge::Bottom => '▀',
278 Edge::Left => '▌',
279 Edge::Right => '▐',
280 },
281 Self::HalfBlockTop => '▄',
282 Self::HalfBlockBottom => '▀',
283 Self::HalfBlockLeft => '▌',
284 Self::HalfBlockRight => '▐',
285 Self::CapTop => '╻',
286 Self::CapBottom => '╹',
287 Self::CapLeft => '╺',
288 Self::CapRight => '╸',
289 Self::CapTopHeavy => '╿',
290 Self::CapBottomHeavy => '╽',
291 Self::CapLeftHeavy => '╾',
292 Self::CapRightHeavy => '╼',
293 Self::Custom(ch) => ch,
294 }
295 }
296}
297
298#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
300pub struct EdgeDecoration {
301 pub edge: Edge,
303 pub placement: DecorationPlacement,
305 pub thickness: u16,
307 pub glyph: DecorationGlyph,
309 pub style: Style,
311 pub focus_style: Option<Style>,
313 pub hover_style: Option<Style>,
315 pub cap_start: Option<DecorationGlyph>,
317 pub cap_end: Option<DecorationGlyph>,
319}
320
321impl EdgeDecoration {
322 pub fn new(edge: Edge) -> Self {
324 Self {
325 edge,
326 placement: DecorationPlacement::Border,
327 thickness: 1,
328 glyph: DecorationGlyph::default(),
329 style: Style::default(),
330 focus_style: None,
331 hover_style: None,
332 cap_start: None,
333 cap_end: None,
334 }
335 }
336
337 pub fn glyph(mut self, glyph: DecorationGlyph) -> Self {
339 self.glyph = glyph;
340 self
341 }
342
343 pub fn thickness(mut self, thickness: u16) -> Self {
345 self.thickness = thickness.max(1);
346 self
347 }
348
349 pub fn style(mut self, style: Style) -> Self {
351 self.style = style;
352 self
353 }
354
355 pub fn focus_style(mut self, style: Style) -> Self {
357 self.focus_style = Some(style);
358 self
359 }
360
361 pub fn hover_style(mut self, style: Style) -> Self {
363 self.hover_style = Some(style);
364 self
365 }
366
367 pub fn cap_start(mut self, glyph: DecorationGlyph) -> Self {
369 self.cap_start = Some(glyph);
370 self
371 }
372
373 pub fn cap_end(mut self, glyph: DecorationGlyph) -> Self {
375 self.cap_end = Some(glyph);
376 self
377 }
378
379 pub fn placement(mut self, placement: DecorationPlacement) -> Self {
381 self.placement = placement;
382 self
383 }
384}
385
386#[derive(Clone, Default)]
388pub struct Frame {
389 pub(crate) props: FrameNode,
391 pub(crate) header: Option<Box<Element>>,
392 pub(crate) child: Option<Box<Element>>,
394}
395
396impl Frame {
397 pub fn new() -> Self {
399 Self::default()
400 }
401
402 pub fn tab_titles<I, S>(mut self, titles: I) -> Self
404 where
405 I: IntoIterator<Item = S>,
406 S: Into<RichText>,
407 {
408 self.props.tab_titles = titles.into_iter().map(Into::into).collect();
409 self
410 }
411
412 pub fn active_tab(mut self, active_tab: usize) -> Self {
414 self.props.active_tab = active_tab;
415 self
416 }
417
418 pub fn active_tab_style(mut self, style: Style) -> Self {
420 self.props.active_tab_style = style;
421 self
422 }
423
424 pub fn focus_active_tab_style(mut self, style: Style) -> Self {
426 self.props.overrides_mut().focus_active_tab_style = Some(style);
427 self
428 }
429
430 pub fn inactive_tab_style(mut self, style: Style) -> Self {
432 self.props.inactive_tab_style = style;
433 self
434 }
435
436 pub fn focus_inactive_tab_style(mut self, style: Style) -> Self {
438 self.props.overrides_mut().focus_inactive_tab_style = Some(style);
439 self
440 }
441
442 pub fn on_tab_change(mut self, cb: Callback<TabsEvent>) -> Self {
444 self.props.on_tab_change = Some(cb);
445 self
446 }
447
448 pub fn tab_variant(mut self, variant: TabVariant) -> Self {
450 self.props.tab_variant = variant;
451 self
452 }
453
454 pub fn header(mut self, header: BorderLabels) -> Self {
456 self.props.header = Box::new(header);
457 self
458 }
459
460 pub fn header_left(mut self, label: impl Into<FrameLabel>) -> Self {
462 self.props.header.left = Some(label.into());
463 self
464 }
465
466 pub fn header_center(mut self, label: impl Into<FrameLabel>) -> Self {
468 self.props.header.center = Some(label.into());
469 self
470 }
471
472 pub fn header_right(mut self, label: impl Into<FrameLabel>) -> Self {
474 self.props.header.right = Some(label.into());
475 self
476 }
477
478 pub fn style(mut self, style: Style) -> Self {
480 self.props.style = style;
481 self
482 }
483
484 pub fn inner_style(mut self, style: Style) -> Self {
486 self.props.overrides_mut().inner_style = Some(style);
487 self
488 }
489
490 pub fn focus_style(mut self, style: Style) -> Self {
492 self.props.overrides_mut().focus_style = Some(StyleSlot::Replace(style));
493 self
494 }
495
496 pub fn extend_focus_style(mut self, style: Style) -> Self {
498 self.props.overrides_mut().focus_style = Some(StyleSlot::Extend(style));
499 self
500 }
501
502 pub fn inherit_focus_style(mut self) -> Self {
504 self.props.overrides_mut().focus_style = Some(StyleSlot::Inherit);
505 self
506 }
507
508 pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
510 self.props.overrides_mut().focus_style = Some(slot);
511 self
512 }
513
514 pub fn hover_style(mut self, style: Style) -> Self {
516 self.props.overrides_mut().hover_style = Some(StyleSlot::Replace(style));
517 self
518 }
519
520 pub fn extend_hover_style(mut self, style: Style) -> Self {
522 self.props.overrides_mut().hover_style = Some(StyleSlot::Extend(style));
523 self
524 }
525
526 pub fn inherit_hover_style(mut self) -> Self {
528 self.props.overrides_mut().hover_style = Some(StyleSlot::Inherit);
529 self
530 }
531
532 pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
534 self.props.overrides_mut().hover_style = Some(slot);
535 self
536 }
537
538 pub fn focus_border_style(mut self, border_style: BorderStyle) -> Self {
540 self.props.overrides_mut().focus_border_style = Some(border_style);
541 self
542 }
543
544 pub fn border(mut self, border: bool) -> Self {
546 self.props.border = border;
547 self
548 }
549
550 pub fn border_style(mut self, border_style: BorderStyle) -> Self {
552 self.props.border_style = border_style;
553 self
554 }
555
556 pub fn border_edges(mut self, border_edges: BorderEdges) -> Self {
561 self.props.border_edges = border_edges;
562 self
563 }
564
565 pub fn border_merge_mode(mut self, merge_mode: BorderMergeMode) -> Self {
567 self.props.border_merge_mode = merge_mode;
568 self
569 }
570
571 pub fn join_frame(mut self, join: bool) -> Self {
573 self.props.join_frame = join;
574 self
575 }
576
577 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
579 self.props.padding = padding.into();
580 self
581 }
582
583 pub fn decoration(mut self, decoration: EdgeDecoration) -> Self {
585 self.props.decorations.push(decoration);
586 self
587 }
588
589 pub fn decorations(mut self, decorations: Vec<EdgeDecoration>) -> Self {
591 self.props.decorations = decorations;
592 self
593 }
594
595 pub fn header_content(mut self, header: impl Into<Element>) -> Self {
597 self.header = Some(Box::new(header.into()));
598 self.props.has_header = true;
599 self
600 }
601
602 pub fn child(mut self, child: impl Into<Element>) -> Self {
604 self.child = Some(Box::new(child.into()));
605 self
606 }
607
608 pub fn width(mut self, width: Length) -> Self {
610 self.props.width = width;
611 self
612 }
613
614 pub fn height(mut self, height: Length) -> Self {
616 self.props.height = height;
617 self
618 }
619
620 pub fn unfocused_height(mut self, height: Length) -> Self {
622 self.props.unfocused_height = Some(height);
623 self
624 }
625
626 pub fn focus_min_height(mut self, height: u16) -> Self {
628 self.props.focus_min_height = Some(height);
629 self
630 }
631
632 pub fn compact(mut self, compact: bool) -> Self {
638 self.props.compact = compact;
639 self
640 }
641
642 pub fn collapsible(mut self, collapsible: bool) -> Self {
644 self.props.collapsible = collapsible;
645 self
646 }
647
648 pub fn footer(mut self, footer: BorderLabels) -> Self {
650 self.props.footer = Box::new(footer);
651 self
652 }
653
654 pub fn footer_left(mut self, label: impl Into<FrameLabel>) -> Self {
656 self.props.footer.left = Some(label.into());
657 self
658 }
659
660 pub fn footer_center(mut self, label: impl Into<FrameLabel>) -> Self {
662 self.props.footer.center = Some(label.into());
663 self
664 }
665
666 pub fn footer_right(mut self, label: impl Into<FrameLabel>) -> Self {
668 self.props.footer.right = Some(label.into());
669 self
670 }
671
672 pub fn header_style(mut self, style: Style) -> Self {
674 self.props.header.style = style;
675 self
676 }
677
678 pub fn focused_header_style(mut self, style: Style) -> Self {
680 self.props.header.focused_style = Some(style);
681 self
682 }
683
684 pub fn footer_style(mut self, style: Style) -> Self {
686 self.props.footer.style = style;
687 self
688 }
689
690 pub fn focused_footer_style(mut self, style: Style) -> Self {
692 self.props.footer.focused_style = Some(style);
693 self
694 }
695
696 pub fn header_padding(mut self, padding: impl Into<Padding>) -> Self {
698 self.props.header.padding = padding.into();
699 self
700 }
701
702 pub fn footer_padding(mut self, padding: impl Into<Padding>) -> Self {
704 self.props.footer.padding = padding.into();
705 self
706 }
707 pub fn focusable(mut self, focusable: bool) -> Self {
709 self.props.focusable = focusable;
710 self
711 }
712
713 pub fn focus_scope(mut self, scope: crate::widgets::FocusScope) -> Self {
715 self.props.focus_scope = scope;
716 self
717 }
718
719 pub fn child_align(mut self, align: Align) -> Self {
721 self.props.child_align = align;
722 self
723 }
724}
725
726impl From<Frame> for Element {
727 fn from(mut value: Frame) -> Self {
728 if value.header.is_some() {
729 value.props.has_header = true;
730 }
731
732 let is_flex_h = matches!(value.props.height, Length::Flex(_) | Length::Percent(_));
736 let is_flex_w = matches!(value.props.width, Length::Flex(_));
737 let is_auto_h = matches!(value.props.height, Length::Auto);
738 let auto_h_depends_on_width = is_auto_h
739 && (value
740 .child
741 .as_deref()
742 .is_some_and(crate::widgets::scroll_child_height_depends_on_width)
743 || value
744 .header
745 .as_deref()
746 .is_some_and(crate::widgets::scroll_child_height_depends_on_width));
747
748 let geometry = measure_frame(&value, None, None);
749 let (_, chrome_h) = measure_frame_chrome(&value);
750
751 let min_w = if is_flex_w {
752 value
753 .props
754 .decoration_outside_padding()
755 .horizontal()
756 .saturating_add(value.props.decoration_border_content_inset().horizontal())
757 } else {
758 geometry.outer_size().0
759 };
760 let min_h = if is_flex_h || auto_h_depends_on_width {
761 chrome_h
762 } else {
763 geometry.outer_size().1
764 };
765
766 let mut layout = LayoutConstraints::default()
767 .min_width(Length::Px(min_w))
768 .min_height(Length::Px(min_h));
769
770 let has_border = value.props.border;
771 if has_border && value.props.collapsible {
772 layout.collapse_h = Some(3);
773 }
774 if let Some(min_h) = value.props.focus_min_height {
775 layout.focus_min_h = if let Length::Px(px) = layout.min_h {
776 px.max(min_h)
777 } else {
778 min_h
779 };
780 }
781 if value.props.compact {
782 layout.force_compact = true;
783 layout.collapse_h = Some(1);
784 }
785 Element::new(ElementKind::Frame(value)).with_layout(layout)
786 }
787}
788
789impl crate::layout::hash::LayoutHash for Frame {
790 fn layout_hash(
791 &self,
792 hasher: &mut impl std::hash::Hasher,
793 recurse: &dyn Fn(&Element) -> Option<u64>,
794 ) -> Option<()> {
795 use std::hash::Hash;
796 self.props.width.hash(hasher);
797 self.props.height.hash(hasher);
798 self.props.unfocused_height.hash(hasher);
799 self.props.focus_min_height.hash(hasher);
800 self.props.border.hash(hasher);
801 self.props.border_style.hash(hasher);
802 self.props.border_edges.hash(hasher);
803 self.props.border_merge_mode.hash(hasher);
804 self.props.join_frame.hash(hasher);
805 self.props.padding.hash(hasher);
806 self.props.compact.hash(hasher);
807 self.props.collapsible.hash(hasher);
808 self.props.child_align.hash(hasher);
809 self.props.decorations.len().hash(hasher);
810 for decoration in &self.props.decorations {
811 decoration.edge.hash(hasher);
812 decoration.placement.hash(hasher);
813 decoration.thickness.hash(hasher);
814 decoration.glyph.hash(hasher);
815 decoration.cap_start.hash(hasher);
816 decoration.cap_end.hash(hasher);
817 }
818 hash_border_labels(&self.props.header, hasher);
819 hash_border_labels(&self.props.footer, hasher);
820 self.props.tab_titles.len().hash(hasher);
821 for tab in &self.props.tab_titles {
822 crate::layout::hash::hash_spans_content(&tab.spans, hasher);
823 }
824 self.props.active_tab.hash(hasher);
825 self.props.tab_variant.hash(hasher);
826 self.header.is_some().hash(hasher);
827 if let Some(header) = self.header.as_deref() {
828 recurse(header)?.hash(hasher);
829 } else {
830 0u8.hash(hasher);
831 }
832 if let Some(child) = self.child.as_deref() {
833 recurse(child)?.hash(hasher);
834 } else {
835 0u8.hash(hasher);
836 }
837 Some(())
838 }
839}
840
841fn hash_optional_rich_text_content(text: Option<&RichText>, hasher: &mut impl std::hash::Hasher) {
842 use std::hash::Hash;
843 text.is_some().hash(hasher);
844 if let Some(text) = text {
845 crate::layout::hash::hash_spans_content(&text.spans, hasher);
846 }
847}
848
849fn hash_border_labels(labels: &BorderLabels, hasher: &mut impl std::hash::Hasher) {
850 use std::hash::Hash;
851
852 for label in [&labels.left, &labels.center, &labels.right]
853 .into_iter()
854 .flatten()
855 {
856 hash_optional_rich_text_content(Some(&label.content), hasher);
857 }
858 labels.padding.hash(hasher);
859}
860
861#[cfg(test)]
862mod tests {
863 use super::{BorderLabels, Frame, FrameLabel};
864 use crate::style::{Color, Padding, Style};
865
866 #[test]
867 fn grouped_frame_builder_stores_all_positions_and_styles() {
868 let header_style = Style::new().fg(Color::Cyan);
869 let focused_header_style = Style::new().bold();
870 let left_style = Style::new().fg(Color::Yellow);
871 let focused_left_style = Style::new().underline();
872 let frame = Frame::new().header(
873 BorderLabels::new()
874 .left(
875 FrameLabel::new("left")
876 .style(left_style)
877 .focused_style(focused_left_style),
878 )
879 .center("center")
880 .right("right")
881 .style(header_style)
882 .focused_style(focused_header_style)
883 .padding(1),
884 );
885
886 let header = &frame.props.header;
887 assert_eq!(
888 header.left.as_ref().and_then(|label| label.style),
889 Some(left_style)
890 );
891 assert_eq!(
892 header.left.as_ref().and_then(|label| label.focused_style),
893 Some(focused_left_style)
894 );
895 assert_eq!(
896 header
897 .center
898 .as_ref()
899 .map(|label| label.content.plain_content()),
900 Some("center".into())
901 );
902 assert_eq!(
903 header
904 .right
905 .as_ref()
906 .map(|label| label.content.plain_content()),
907 Some("right".into())
908 );
909 assert_eq!(header.style, header_style);
910 assert_eq!(header.focused_style, Some(focused_header_style));
911 assert_eq!(header.padding, Padding::from(1));
912 }
913
914 #[test]
915 fn repeated_group_setters_replace_the_previous_group() {
916 let frame = Frame::new()
917 .header(BorderLabels::new().left("old").right("removed"))
918 .footer(BorderLabels::new().center("old footer"))
919 .header(BorderLabels::new().center("new"))
920 .footer(BorderLabels::new().right("new footer"));
921
922 assert!(frame.props.header.left.is_none());
923 assert_eq!(
924 frame
925 .props
926 .header
927 .center
928 .as_ref()
929 .map(|label| label.content.plain_content()),
930 Some("new".into())
931 );
932 assert!(frame.props.footer.center.is_none());
933 assert_eq!(
934 frame
935 .props
936 .footer
937 .right
938 .as_ref()
939 .map(|label| label.content.plain_content()),
940 Some("new footer".into())
941 );
942 }
943
944 #[test]
945 fn label_width_uses_display_width_and_padding() {
946 let labels = BorderLabels::new().left("界").padding(1);
947
948 assert_eq!(labels.min_width(), 4);
949 }
950}