1use super::*;
2use crate::canvas::*;
3
4use crossterm::event::{KeyCode, KeyModifiers};
5use std::rc::Rc;
6use uid::IdU64;
7
8pub type TabViewSelectionChangedCallback = dyn Fn(&mut TabView);
9pub type TabViewKeyHandler = fn(&mut TabView, event: &mut WindowEvent);
10
11pub struct Tab {
12 name: String,
13 content: WindowRef,
14 id: IdU64<Tab>,
15}
16
17impl Tab {
18 pub fn new<S: ToString>(name: S, content: WindowRef) -> Self {
20 Self {
21 name: name.to_string(),
22 content,
23 id: IdU64::new(),
24 }
25 }
26
27 pub fn content(&self) -> &WindowRef {
29 &self.content
30 }
31}
32
33impl PartialEq for Tab {
35 fn eq(&self, other: &Self) -> bool {
36 self.id == other.id
37 }
38}
39
40#[derive(Default)]
41struct TabViewColors {
42 fg_highlight: Option<Color>,
43 bg_highlight: Option<Color>,
44}
45
46pub struct TabView {
61 base: WidgetBase,
62 tabs: Vec<Tab>,
63 selected: usize,
64 enumerate_tabs: bool,
65 callback: Rc<TabViewSelectionChangedCallback>,
66 colors: TabViewColors,
67 key_handler: TabViewKeyHandler,
68}
69
70impl Default for TabView {
71 fn default() -> Self {
72 Self {
73 base: WidgetBase::default(),
74 tabs: Vec::default(),
75 selected: usize::MIN,
76 enumerate_tabs: false,
77 callback: Rc::new(Self::default_callback),
78 colors: TabViewColors::default(),
79 key_handler: Self::default_key_handler,
80 }
81 }
82}
83
84impl ItemCollection<Tab> for TabView {
85 type Index = usize;
86
87 fn add_item(&mut self, item: Tab) {
88 self.tabs.push(item);
89 }
90
91 fn remove_item(&mut self, item: &Tab) {
92 let idx = self.tabs.iter().position(|x| x == item);
93 if let Some(index) = idx {
94 if self.selected == index {
95 self.selected = usize::MIN;
96 self.provoke_changed_property(WindowProperty::Focus);
97 }
98 self.tabs.remove(index);
99 self.provoke_changed_property(WindowProperty::Children);
100 }
101 }
102
103 fn remove_at(&mut self, index: Self::Index) -> Tab {
104 self.tabs.remove(index)
105 }
106
107 fn get_item(&mut self, index: Self::Index) -> Option<&Tab> {
108 self.tabs.get(index)
109 }
110
111 fn clear_items(&mut self) {
112 self.tabs.clear();
113 self.selected = usize::MIN;
114 self.provoke_changed_property(WindowProperty::Children);
115 }
116
117 fn items(&self) -> Self::Index {
118 self.tabs.len()
119 }
120}
121
122impl TabView {
123 pub const TAB_SEPERATOR: Grapheme = BorderKind::Solid.line_style()[1];
124 pub const BRACKET_OPEN: Grapheme = Grapheme::new_unchecked("[", GlyphWidth::Half);
125 pub const BRACKET_CLOSE: Grapheme = Grapheme::new_unchecked("]", GlyphWidth::Half);
126 pub const ANGLES_RIGHT: Grapheme = Grapheme::new_unchecked("»", GlyphWidth::Half);
127 pub const ANGLES_LEFT: Grapheme = Grapheme::new_unchecked("«", GlyphWidth::Half);
128 pub const MIN_TAB_WIDTH: TSize = 4;
129
130 pub fn default_callback(&mut self) {}
131
132 pub fn default_key_handler(&mut self, event: &mut WindowEvent) {
133 if let Event::Key(k) = event.raw() {
134 if self.base.enabled && k.modifiers.contains(KeyModifiers::CONTROL) {
135 if k.code == KeyCode::Char('x') {
136 self.select_previous();
137 event.handled = true;
138 }
139 else if k.code == KeyCode::Char('y') {
140 self.select_next();
141 event.handled = true;
142 }
143 }
144 }
145 }
146
147 pub fn set_selection_changed_callback<F: Fn(&mut Self) + 'static>(&mut self, callback: F) {
148 self.callback = Rc::new(callback);
149 }
150
151 pub fn set_key_handler(&mut self, f: TabViewKeyHandler) {
152 self.key_handler = f;
153 }
154
155 pub fn select_tab(&mut self, tab: &Tab) {
157 if let Some(idx) = self.tabs.iter().position(|x| x.id == tab.id)
158 && idx != self.selected
159 {
160 self.selected = idx;
161 self.selection_changed();
162 }
163 }
164
165 pub fn enable_tab_enumeration(&mut self) {
168 self.enumerate_tabs = true;
169 }
170
171 pub fn disable_tab_enumeration(&mut self) {
173 self.enumerate_tabs = false;
174 }
175
176 pub fn get_selection(&self) -> Option<&Tab> {
178 self.tabs.get(self.selected)
179 }
180
181 pub fn set_highlight_fg_color(&mut self, color: Option<Color>) {
183 self.colors.fg_highlight = color;
184 }
185
186 pub fn set_highlight_bg_color(&mut self, color: Option<Color>) {
188 self.colors.bg_highlight = color;
189 }
190
191 pub fn select_next(&mut self) {
192 if self.selected < self.tabs.len() - 1 {
193 self.selected += 1;
194 self.selection_changed();
195 }
196 }
197
198 pub fn select_previous(&mut self) {
199 if self.selected > 0 {
200 self.selected -= 1;
201 self.selection_changed();
202 }
203 }
204
205 fn selection_changed(&mut self) {
206 self.provoke_changed_property(WindowProperty::Children);
207 self.provoke_changed_property(WindowProperty::Focus);
208 let cb = self.callback.clone();
209 cb(self);
210 }
211}
212
213impl Window for TabView {
214 fn children(&mut self, mut builder: SubWindowBuilder) -> SubWindows {
215 let base_rect = builder.base_rect();
216 if let Some(selection) = self.get_selection() {
217 builder
218 .add_rect(
219 base_rect
220 .subrect(0, 1, base_rect.size().x, base_rect.size().y - 1)
221 .unwrap(),
222 )
223 .unwrap();
224 builder.add_child(selection.content.clone());
225 }
226 builder.build().unwrap()
227 }
228
229 fn render(&self, canvas: &mut crate::canvas::Canvas) {
230 let width = canvas.size().x;
231 let mut row = canvas.get_row_variable_width(0).unwrap();
232 let selected = self.tabs.get(self.selected);
233 let mut size_per_tab = TSize::max(Self::MIN_TAB_WIDTH, width / self.tabs.len() as TSize);
234 let tabs_per_page = width / size_per_tab - 1;
235 let skip = self.selected / tabs_per_page as usize * tabs_per_page as usize;
236
237 let mut x = 0;
238 if skip > usize::MIN {
240 size_per_tab = TSize::max(Self::MIN_TAB_WIDTH, width / self.tabs.len() as TSize);
241 row.add_grapheme(
242 Self::ANGLES_LEFT,
243 self.base.colors.base_fg,
244 self.base.colors.base_bg,
245 Style::empty(),
246 )
247 .ok();
248 x += 1;
249 }
250
251 for (n, tab) in self.tabs.iter().skip(skip).enumerate() {
252 row = row.with_custom_width(x + size_per_tab).unwrap();
253 row.skip(x);
254
255 let fg_color;
256 let bg_color;
257
258 match selected.unwrap() == tab {
259 true => {
260 row.set_style(Style::Underline);
261 fg_color = self.colors.fg_highlight.or(self.base.colors.base_fg);
262 bg_color = self.colors.bg_highlight.or(self.base.colors.base_bg);
263 }
264 false => {
265 fg_color = self.base.colors.base_fg;
266 bg_color = self.base.colors.base_bg;
267 }
268 }
269
270 if width - x <= size_per_tab * 2 {
272 row.add_grapheme(Self::ANGLES_RIGHT, fg_color, bg_color, Style::None)
273 .unwrap();
274 break;
275 }
276
277 if n > 0 {
278 row.add_grapheme(Self::TAB_SEPERATOR, fg_color, bg_color, Style::None)
279 .ok();
280 }
281
282 if self.enumerate_tabs {
283 row.add_grapheme(Self::BRACKET_OPEN, fg_color, bg_color, Style::None)
284 .ok();
285 let tab_n = (skip + n + 1).to_string();
286 row.add_string(
287 &tab_n,
288 fg_color,
289 bg_color,
290 Style::None,
291 Some(VariableWidthGlyphRow::DOT3_REPLACEMENT),
292 )
293 .ok();
294 row.add_grapheme(Self::BRACKET_CLOSE, fg_color, bg_color, Style::None)
295 .ok();
296 row.add_grapheme(Grapheme::PLACEHOLDER, fg_color, bg_color, Style::None)
297 .ok();
298 }
299
300 row.add_string(
301 &tab.name,
302 fg_color,
303 bg_color,
304 Style::None,
305 Some(VariableWidthGlyphRow::DOT3_REPLACEMENT),
306 )
307 .ok();
308 x += size_per_tab;
309 }
310 }
311
312 fn handle_event(&mut self, event: &mut WindowEvent) {
313 (self.key_handler)(self, event);
314 }
315
316
317 fn focus(&self) -> Option<WindowRef> {
318 self.get_selection().map(|x| x.content.clone())
319 }
320
321 fn is_enabled(&self) -> bool {
322 self.base.enabled
323 }
324}
325
326impl HasWindowUID for TabView {
327 fn uid(&self) -> WindowUID {
328 self.base.uid
329 }
330}
331
332impl WindowLayout for TabView {
333 fn border(&self) -> BorderStyle {
334 self.base.border
335 }
336
337 fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
338 self.base.alignment
339 }
340
341 fn is_visible(&self) -> bool {
342 self.base.visibility
343 }
344
345 fn margin(&self) -> Thickness {
346 self.base.margin
347 }
348}
349
350impl Widget for TabView {
351 fn set_alignment(&mut self, horizontal: HorizontalAlignment, vertical: VerticalAlignment) {
352 self.base.alignment = (horizontal, vertical);
353 self.provoke_changed_property(WindowProperty::Alignment);
354 }
355
356 fn set_visibility(&mut self, visibility: bool) {
357 self.base.visibility = visibility;
358 self.provoke_changed_property(WindowProperty::IsVisible);
359 }
360
361 fn set_width(&mut self, width: super::Size) {
362 self.base.size.x = width;
363 self.provoke_changed_property(WindowProperty::Size);
364 }
365
366 fn set_height(&mut self, height: super::Size) {
367 self.base.size.y = height;
368 }
369
370 fn set_margin(&mut self, margin: Thickness) {
371 self.base.margin = margin;
372 self.provoke_changed_property(WindowProperty::Margin);
373 }
374
375 fn set_border(&mut self, border: BorderStyle) {
376 self.base.border = border;
377 self.provoke_changed_property(WindowProperty::Border);
378 }
379
380 fn set_enabled_state(&mut self, is_enabled: bool) {
381 self.base.enabled = is_enabled;
382 }
383
384 fn set_width_constraint(&mut self, width: SizeConstraint) {
385 self.base.constraints.x = width;
386 self.provoke_changed_property(WindowProperty::Size);
387 }
388
389 fn set_height_constraint(&mut self, height: SizeConstraint) {
390 self.base.constraints.y = height;
391 self.provoke_changed_property(WindowProperty::Size);
392 }
393}
394
395impl WidgetColors for TabView {
396 fn set_disabled_color(&mut self, color: Option<Color>) {
397 self.base.colors.disabled = color;
398 }
399
400 fn set_base_fg_color(&mut self, color: Option<Color>) {
401 self.base.colors.base_fg = color;
402 }
403
404 fn set_base_bg_color(&mut self, color: Option<Color>) {
405 self.base.colors.base_bg = color;
406 }
407}