返回 CodeWhale
dock_tabs.rs
1 //! Native work-dock tab fitting and painting. The host supplies availability,
2 //! counts, actual close copy and interaction state; this owns no focus policy.
3
4 use super::{WorkbarPanel, WorkbarTab};
5 use ratatui::{
6 buffer::Buffer,
7 layout::Rect,
8 style::Style,
9 text::{Line, Span},
10 widgets::{Paragraph, Widget},
11 };
12 use std::borrow::Cow;
13
14 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
15 pub enum DockTabTarget {
16 Panel(WorkbarPanel),
17 Close,
18 }
19
20 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21 pub struct DockTabStyles {
22 pub idle: Style,
23 pub active: Style,
24 pub hovered: Style,
25 pub close: Style,
26 pub close_hovered: Style,
27 }
28
29 #[derive(Clone, Debug, PartialEq, Eq)]
30 pub struct DockTabEntry {
31 pub tab: WorkbarTab,
32 pub area: Rect,
33 pub label: String,
34 pub style: Style,
35 }
36
37 #[derive(Clone, Debug, PartialEq, Eq)]
38 pub struct DockTabPlan {
39 pub tabs: Vec<DockTabEntry>,
40 pub close_area: Rect,
41 pub close: String,
42 close_style: Style,
43 }
44
45 impl DockTabPlan {
46 #[must_use]
47 pub fn hitboxes(&self) -> Vec<(DockTabTarget, Rect)> {
48 let mut boxes = self
49 .tabs
50 .iter()
51 .filter(|tab| !tab.area.is_empty())
52 .map(|tab| (DockTabTarget::Panel(tab.tab.panel), tab.area))
53 .collect::<Vec<_>>();
54 if !self.close_area.is_empty() {
55 boxes.push((DockTabTarget::Close, self.close_area));
56 }
57 boxes
58 }
59
60 /// Paint this exact plan; hitboxes and painted cells share one snapshot.
61 pub fn paint(&self, buf: &mut Buffer) {
62 for tab in &self.tabs {
63 Paragraph::new(Line::from(Span::styled(tab.label.as_str(), tab.style)))
64 .render(tab.area, buf);
65 }
66 Paragraph::new(Line::from(Span::styled(
67 self.close.clone(),
68 self.close_style,
69 )))
70 .render(self.close_area, buf);
71 }
72 }
73
74 /// One native row. All styles are supplied by the live host theme, including
75 /// backgrounds and modifiers. Close copy describes the host's actual action.
76 #[derive(Clone, Debug)]
77 pub struct DockTabRow<'a> {
78 pub tabs: &'a [WorkbarTab],
79 pub active: WorkbarPanel,
80 pub bottom: bool,
81 pub close: Cow<'a, str>,
82 pub hovered: Option<DockTabTarget>,
83 pub pressed: Option<WorkbarPanel>,
84 pub styles: DockTabStyles,
85 }
86
87 impl DockTabRow<'_> {
88 #[must_use]
89 pub fn plan(&self, area: Rect) -> DockTabPlan {
90 let width = usize::from(area.width);
91 let close_width =
92 unicode_width::UnicodeWidthStr::width(self.close.as_ref()).min(width) as u16;
93 let y = if self.bottom {
94 area.y
95 .saturating_add(1)
96 .min(area.bottom().saturating_sub(1))
97 } else {
98 area.y
99 };
100 let close_area = Rect::new(
101 area.right().saturating_sub(close_width),
102 y,
103 close_width,
104 u16::from(!area.is_empty()),
105 );
106 let mut plan = DockTabPlan {
107 tabs: Vec::new(),
108 close_area,
109 close: self.close.to_string(),
110 close_style: if self.hovered == Some(DockTabTarget::Close) {
111 self.styles.close_hovered
112 } else {
113 self.styles.close
114 },
115 };
116 if area.is_empty() {
117 return plan;
118 }
119 let mut entries = WorkbarPanel::ORDER
120 .into_iter()
121 .filter_map(|panel| self.tabs.iter().find(|tab| tab.panel == panel).cloned())
122 .collect::<Vec<_>>();
123 if !entries.iter().any(|tab| tab.panel == self.active) {
124 entries.push(WorkbarTab::new(self.active));
125 entries.sort_by_key(|tab| {
126 WorkbarPanel::ORDER
127 .iter()
128 .position(|panel| *panel == tab.panel)
129 });
130 }
131 let fits = |tabs: &[WorkbarTab], counts: bool| {
132 tabs.iter()
133 .map(|tab| {
134 unicode_width::UnicodeWidthStr::width(tab.panel.label())
135 + if counts && tab.count.is_some_and(|n| n > 0) {
136 1 + tab.count.unwrap_or(0).to_string().len()
137 } else {
138 0
139 }
140 + 2
141 })
142 .sum::<usize>()
143 .saturating_add(tabs.len().saturating_sub(1).saturating_mul(2))
144 .saturating_add(usize::from(close_width) + 2)
145 <= width
146 };
147 let show_counts = fits(&entries, true);
148 while !fits(&entries, show_counts) && entries.len() > 1 {
149 let Some(index) = entries.iter().rposition(|tab| tab.panel != self.active) else {
150 break;
151 };
152 entries.remove(index);
153 }
154 let mut x = area.x.saturating_add(1);
155 for tab in entries {
156 let label = if show_counts && tab.count.is_some_and(|n| n > 0) {
157 format!("{} {}", tab.panel.label(), tab.count.unwrap_or(0))
158 } else {
159 tab.panel.label().to_string()
160 };
161 let tab_width = u16::try_from(
162 unicode_width::UnicodeWidthStr::width(label.as_str()).saturating_add(2),
163 )
164 .unwrap_or(u16::MAX)
165 .min(area.width);
166 if x.saturating_add(tab_width) > close_area.x {
167 break;
168 }
169 let style = if tab.panel == self.active || self.pressed == Some(tab.panel) {
170 self.styles.active
171 } else if self.hovered == Some(DockTabTarget::Panel(tab.panel)) {
172 self.styles.hovered
173 } else {
174 self.styles.idle
175 };
176 plan.tabs.push(DockTabEntry {
177 tab,
178 area: Rect::new(x, y, tab_width, 1),
179 label: format!(" {label} "),
180 style,
181 });
182 x = x.saturating_add(tab_width).saturating_add(2);
183 }
184 plan
185 }
186 }
187
188 impl Widget for &DockTabRow<'_> {
189 fn render(self, area: Rect, buf: &mut Buffer) {
190 self.plan(area).paint(buf);
191 }
192 }
193
193 lines RUST