Skip to main content

tui_lipan/widgets/frame/
mod.rs

1//! Frame widget.
2
3pub 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;
21// Internal renderer alias.
22pub(crate) use self::node::FrameProps;
23
24/// A label rendered in one of a frame's border positions.
25#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
26pub struct FrameLabel {
27    /// Label content.
28    pub content: RichText,
29    /// Optional style layered on top of the containing group style.
30    pub style: Option<Style>,
31    /// Optional style layered on top of the group focus style while focused.
32    pub focused_style: Option<Style>,
33}
34
35impl FrameLabel {
36    /// Create a border label.
37    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    /// Set the normal label style.
46    pub fn style(mut self, style: Style) -> Self {
47        self.style = Some(style);
48        self
49    }
50
51    /// Set the label style while its frame is focused.
52    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/// Positional labels rendered in one border row of a frame.
89#[derive(Clone, Debug, Default, PartialEq, Eq)]
90pub struct BorderLabels {
91    /// Left-aligned label.
92    pub left: Option<FrameLabel>,
93    /// Center-aligned label.
94    pub center: Option<FrameLabel>,
95    /// Right-aligned label.
96    pub right: Option<FrameLabel>,
97    /// Default style for labels in this group.
98    pub style: Style,
99    /// Optional style layered on top while the frame is focused.
100    pub focused_style: Option<Style>,
101    /// Horizontal padding around each label.
102    pub padding: Padding,
103}
104
105impl BorderLabels {
106    /// Create an empty label group.
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    /// Set the left label.
112    pub fn left(mut self, label: impl Into<FrameLabel>) -> Self {
113        self.left = Some(label.into());
114        self
115    }
116
117    /// Set the centered label.
118    pub fn center(mut self, label: impl Into<FrameLabel>) -> Self {
119        self.center = Some(label.into());
120        self
121    }
122
123    /// Set the right label.
124    pub fn right(mut self, label: impl Into<FrameLabel>) -> Self {
125        self.right = Some(label.into());
126        self
127    }
128
129    /// Set the default style for labels in this group.
130    pub fn style(mut self, style: Style) -> Self {
131        self.style = style;
132        self
133    }
134
135    /// Set the group style while the frame is focused.
136    pub fn focused_style(mut self, style: Style) -> Self {
137        self.focused_style = Some(style);
138        self
139    }
140
141    /// Set horizontal padding around labels in this group.
142    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/// Strategy used when frame border symbols overlap.
170#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
171pub enum BorderMergeMode {
172    /// Last write wins; no symbol merging.
173    Replace,
174    /// Merge only when an exact box-drawing symbol exists.
175    #[default]
176    Exact,
177    /// Merge using closest match when exact merge is unavailable.
178    Fuzzy,
179}
180
181/// Where an edge decoration is drawn.
182#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
183pub enum DecorationPlacement {
184    /// Draw on the frame border line (or outer edge if no border).
185    #[default]
186    Border,
187    /// Draw inside the content area edge (after border + padding).
188    Inside,
189    /// Draw outside the frame content, growing the frame size.
190    Outside,
191}
192
193/// Glyphs used for edge decorations.
194#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
195pub enum DecorationGlyph {
196    /// Auto thin glyph (horizontal/vertical based on edge).
197    AutoThin,
198    /// Auto heavy glyph (horizontal/vertical based on edge).
199    #[default]
200    AutoHeavy,
201    /// Auto double-line glyph (horizontal/vertical based on edge).
202    AutoDouble,
203    /// Auto block glyph (horizontal uses ▬, vertical uses ┃).
204    AutoBlock,
205    /// Horizontal thin line (─).
206    HorizontalThin,
207    /// Horizontal heavy line (━).
208    HorizontalHeavy,
209    /// Horizontal double line (═).
210    HorizontalDouble,
211    /// Horizontal block (▬).
212    HorizontalBlock,
213    /// Vertical thin line (│).
214    VerticalThin,
215    /// Vertical heavy line (┃).
216    VerticalHeavy,
217    /// Vertical double line (║).
218    VerticalDouble,
219    /// Auto half-block glyph (edge-based).
220    HalfBlock,
221    /// Half-block top glyph (▄).
222    HalfBlockTop,
223    /// Half-block bottom glyph (▀).
224    HalfBlockBottom,
225    /// Half-block left glyph (▌).
226    HalfBlockLeft,
227    /// Half-block right glyph (▐).
228    HalfBlockRight,
229    /// Vertical cap top glyph (╻).
230    CapTop,
231    /// Vertical cap bottom glyph (╹).
232    CapBottom,
233    /// Horizontal cap left glyph (╺).
234    CapLeft,
235    /// Horizontal cap right glyph (╸).
236    CapRight,
237    /// Vertical cap top heavy glyph (╿).
238    CapTopHeavy,
239    /// Vertical cap bottom heavy glyph (╽).
240    CapBottomHeavy,
241    /// Horizontal cap left heavy glyph (╾).
242    CapLeftHeavy,
243    /// Horizontal cap right heavy glyph (╼).
244    CapRightHeavy,
245    /// Custom single glyph.
246    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/// Edge decoration descriptor.
299#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
300pub struct EdgeDecoration {
301    /// Target edge for the decoration.
302    pub edge: Edge,
303    /// Placement relative to the frame content.
304    pub placement: DecorationPlacement,
305    /// Thickness in cells (width for vertical, height for horizontal).
306    pub thickness: u16,
307    /// Glyph used to draw the decoration.
308    pub glyph: DecorationGlyph,
309    /// Base style for the decoration.
310    pub style: Style,
311    /// Optional style applied when focused.
312    pub focus_style: Option<Style>,
313    /// Optional style applied when hovered.
314    pub hover_style: Option<Style>,
315    /// Optional glyph for the start cap (top/left).
316    pub cap_start: Option<DecorationGlyph>,
317    /// Optional glyph for the end cap (bottom/right).
318    pub cap_end: Option<DecorationGlyph>,
319}
320
321impl EdgeDecoration {
322    /// Create a new decoration targeting the given edge.
323    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    /// Set the decoration glyph.
338    pub fn glyph(mut self, glyph: DecorationGlyph) -> Self {
339        self.glyph = glyph;
340        self
341    }
342
343    /// Set the decoration thickness in cells.
344    pub fn thickness(mut self, thickness: u16) -> Self {
345        self.thickness = thickness.max(1);
346        self
347    }
348
349    /// Set the base style.
350    pub fn style(mut self, style: Style) -> Self {
351        self.style = style;
352        self
353    }
354
355    /// Set the style used when focused.
356    pub fn focus_style(mut self, style: Style) -> Self {
357        self.focus_style = Some(style);
358        self
359    }
360
361    /// Set the style used when hovered.
362    pub fn hover_style(mut self, style: Style) -> Self {
363        self.hover_style = Some(style);
364        self
365    }
366
367    /// Set the start cap glyph.
368    pub fn cap_start(mut self, glyph: DecorationGlyph) -> Self {
369        self.cap_start = Some(glyph);
370        self
371    }
372
373    /// Set the end cap glyph.
374    pub fn cap_end(mut self, glyph: DecorationGlyph) -> Self {
375        self.cap_end = Some(glyph);
376        self
377    }
378
379    /// Set the placement relative to the frame.
380    pub fn placement(mut self, placement: DecorationPlacement) -> Self {
381        self.placement = placement;
382        self
383    }
384}
385
386/// A frame container (lazygit-style panel).
387#[derive(Clone, Default)]
388pub struct Frame {
389    /// Frame properties.
390    pub(crate) props: FrameNode,
391    pub(crate) header: Option<Box<Element>>,
392    /// Child.
393    pub(crate) child: Option<Box<Element>>,
394}
395
396impl Frame {
397    /// Create a frame.
398    pub fn new() -> Self {
399        Self::default()
400    }
401
402    /// Set tab titles rendered in the top border.
403    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    /// Set the active tab index.
413    pub fn active_tab(mut self, active_tab: usize) -> Self {
414        self.props.active_tab = active_tab;
415        self
416    }
417
418    /// Set the style for the active tab.
419    pub fn active_tab_style(mut self, style: Style) -> Self {
420        self.props.active_tab_style = style;
421        self
422    }
423
424    /// Set the style for the active tab when focused.
425    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    /// Set the style for inactive tabs.
431    pub fn inactive_tab_style(mut self, style: Style) -> Self {
432        self.props.inactive_tab_style = style;
433        self
434    }
435
436    /// Set the style for inactive tabs when focused.
437    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    /// Callback fired when the active tab changes via border tab clicks.
443    pub fn on_tab_change(mut self, cb: Callback<TabsEvent>) -> Self {
444        self.props.on_tab_change = Some(cb);
445        self
446    }
447
448    /// Set the visual variant for border tabs.
449    pub fn tab_variant(mut self, variant: TabVariant) -> Self {
450        self.props.tab_variant = variant;
451        self
452    }
453
454    /// Set labels rendered in the top border.
455    pub fn header(mut self, header: BorderLabels) -> Self {
456        self.props.header = Box::new(header);
457        self
458    }
459
460    /// Set the left header label.
461    pub fn header_left(mut self, label: impl Into<FrameLabel>) -> Self {
462        self.props.header.left = Some(label.into());
463        self
464    }
465
466    /// Set the centered header label.
467    pub fn header_center(mut self, label: impl Into<FrameLabel>) -> Self {
468        self.props.header.center = Some(label.into());
469        self
470    }
471
472    /// Set the right header label.
473    pub fn header_right(mut self, label: impl Into<FrameLabel>) -> Self {
474        self.props.header.right = Some(label.into());
475        self
476    }
477
478    /// Set base style.
479    pub fn style(mut self, style: Style) -> Self {
480        self.props.style = style;
481        self
482    }
483
484    /// Set style for the inner content area (distinct from border).
485    pub fn inner_style(mut self, style: Style) -> Self {
486        self.props.overrides_mut().inner_style = Some(style);
487        self
488    }
489
490    /// Set style applied when the frame or its children have focus.
491    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    /// Extend the themed focus style with the given style.
497    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    /// Inherit focus style from the active theme.
503    pub fn inherit_focus_style(mut self) -> Self {
504        self.props.overrides_mut().focus_style = Some(StyleSlot::Inherit);
505        self
506    }
507
508    /// Set the focus style slot directly.
509    pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
510        self.props.overrides_mut().focus_style = Some(slot);
511        self
512    }
513
514    /// Set style when hovered.
515    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    /// Extend the themed hover style with the given style.
521    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    /// Inherit hover style from the active theme.
527    pub fn inherit_hover_style(mut self) -> Self {
528        self.props.overrides_mut().hover_style = Some(StyleSlot::Inherit);
529        self
530    }
531
532    /// Set the hover style slot directly.
533    pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
534        self.props.overrides_mut().hover_style = Some(slot);
535        self
536    }
537
538    /// Set border style applied when focused.
539    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    /// Enable or disable border decoration.
545    pub fn border(mut self, border: bool) -> Self {
546        self.props.border = border;
547        self
548    }
549
550    /// Set border style.
551    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
552        self.props.border_style = border_style;
553        self
554    }
555
556    /// Set which border edges reserve layout space and render as frame chrome.
557    ///
558    /// `BorderEdges::HorizontalCaps` keeps the top and bottom border rows with
559    /// corner caps, but does not consume left or right content columns.
560    pub fn border_edges(mut self, border_edges: BorderEdges) -> Self {
561        self.props.border_edges = border_edges;
562        self
563    }
564
565    /// Set merge behavior for overlapping frame border symbols.
566    pub fn border_merge_mode(mut self, merge_mode: BorderMergeMode) -> Self {
567        self.props.border_merge_mode = merge_mode;
568        self
569    }
570
571    /// Join borders with neighboring frames when edges touch.
572    pub fn join_frame(mut self, join: bool) -> Self {
573        self.props.join_frame = join;
574        self
575    }
576
577    /// Set padding.
578    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
579        self.props.padding = padding.into();
580        self
581    }
582
583    /// Add an edge decoration.
584    pub fn decoration(mut self, decoration: EdgeDecoration) -> Self {
585        self.props.decorations.push(decoration);
586        self
587    }
588
589    /// Replace all decorations.
590    pub fn decorations(mut self, decorations: Vec<EdgeDecoration>) -> Self {
591        self.props.decorations = decorations;
592        self
593    }
594
595    /// Set a content header element rendered inside the frame.
596    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    /// Set child.
603    pub fn child(mut self, child: impl Into<Element>) -> Self {
604        self.child = Some(Box::new(child.into()));
605        self
606    }
607
608    /// Override requested width.
609    pub fn width(mut self, width: Length) -> Self {
610        self.props.width = width;
611        self
612    }
613
614    /// Override requested height.
615    pub fn height(mut self, height: Length) -> Self {
616        self.props.height = height;
617        self
618    }
619
620    /// Override requested height when not focused.
621    pub fn unfocused_height(mut self, height: Length) -> Self {
622        self.props.unfocused_height = Some(height);
623        self
624    }
625
626    /// Set minimum height when focused (includes borders).
627    pub fn focus_min_height(mut self, height: u16) -> Self {
628        self.props.focus_min_height = Some(height);
629        self
630    }
631
632    /// Enable compact single-line rendering mode.
633    ///
634    /// In compact mode, the frame renders as a single horizontal line with dashes
635    /// and the title embedded: `-[1]-Status-----`. This is useful for collapsed
636    /// panels in a dynamic layout.
637    pub fn compact(mut self, compact: bool) -> Self {
638        self.props.compact = compact;
639        self
640    }
641
642    /// Allow the frame to collapse when space is constrained.
643    pub fn collapsible(mut self, collapsible: bool) -> Self {
644        self.props.collapsible = collapsible;
645        self
646    }
647
648    /// Set labels rendered in the bottom border.
649    pub fn footer(mut self, footer: BorderLabels) -> Self {
650        self.props.footer = Box::new(footer);
651        self
652    }
653
654    /// Set the left footer label.
655    pub fn footer_left(mut self, label: impl Into<FrameLabel>) -> Self {
656        self.props.footer.left = Some(label.into());
657        self
658    }
659
660    /// Set the centered footer label.
661    pub fn footer_center(mut self, label: impl Into<FrameLabel>) -> Self {
662        self.props.footer.center = Some(label.into());
663        self
664    }
665
666    /// Set the right footer label.
667    pub fn footer_right(mut self, label: impl Into<FrameLabel>) -> Self {
668        self.props.footer.right = Some(label.into());
669        self
670    }
671
672    /// Set the header group style.
673    pub fn header_style(mut self, style: Style) -> Self {
674        self.props.header.style = style;
675        self
676    }
677
678    /// Set the focused header group style.
679    pub fn focused_header_style(mut self, style: Style) -> Self {
680        self.props.header.focused_style = Some(style);
681        self
682    }
683
684    /// Set the footer group style.
685    pub fn footer_style(mut self, style: Style) -> Self {
686        self.props.footer.style = style;
687        self
688    }
689
690    /// Set the focused footer group style.
691    pub fn focused_footer_style(mut self, style: Style) -> Self {
692        self.props.footer.focused_style = Some(style);
693        self
694    }
695
696    /// Set header label padding.
697    pub fn header_padding(mut self, padding: impl Into<Padding>) -> Self {
698        self.props.header.padding = padding.into();
699        self
700    }
701
702    /// Set footer label padding.
703    pub fn footer_padding(mut self, padding: impl Into<Padding>) -> Self {
704        self.props.footer.padding = padding.into();
705        self
706    }
707    /// Make the frame focusable even if it has no child or tabs.
708    pub fn focusable(mut self, focusable: bool) -> Self {
709        self.props.focusable = focusable;
710        self
711    }
712
713    /// Set focus traversal behavior for this subtree.
714    pub fn focus_scope(mut self, scope: crate::widgets::FocusScope) -> Self {
715        self.props.focus_scope = scope;
716        self
717    }
718
719    /// Set alignment of child content within the frame's inner area.
720    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        // For Flex frames, keep minimum width close to zero so sibling
733        // frames with equal flex factors can share width evenly regardless
734        // of border/title chrome differences.
735        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}