Skip to main content

flux_tui/components/
tabview.rs

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	/// Creates an unique tab for a window  - the name will be shown in the tab itself
19	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	/// Returns the window inside the [Tab]
28	pub fn content(&self) -> &WindowRef {
29		&self.content
30	}
31}
32
33/// This will only compare the internal unique IDs
34impl 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
46//TODO: tabs alignment
47/// The currently selected tab will have a highlighted background. All others will have the same
48/// background (which is usually darker).
49///
50/// Design:
51///
52/// ```text
53/// [1] Tab│[2] Tab│[3] Tab│...
54///
55///         -------------
56///         Tab 1 content
57///         -------------
58///
59/// ```
60pub 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	/// Forcibly selects the given tab item
156	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	/// This will enumerate each tab depending on the order they are in
166	/// Enumerated Tabs will start with (n), n ∈ ℕ
167	pub fn enable_tab_enumeration(&mut self) {
168		self.enumerate_tabs = true;
169	}
170
171	/// Disables the tab enumeration of all tabs
172	pub fn disable_tab_enumeration(&mut self) {
173		self.enumerate_tabs = false;
174	}
175
176	/// Returns the selected [Tab]
177	pub fn get_selection(&self) -> Option<&Tab> {
178		self.tabs.get(self.selected)
179	}
180
181	/// Sets the foreground for the highlighted tab
182	pub fn set_highlight_fg_color(&mut self, color: Option<Color>) {
183		self.colors.fg_highlight = color;
184	}
185
186	/// Sets the background for the highlighted tab
187	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		// Indicate more tabs on the left
239		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			// Indicate more tabs which cant be shown right now
271			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}