revue/widget/layout/
tabs.rs1use crate::render::Cell;
4use crate::style::Color;
5use crate::utils::Selection;
6use crate::widget::traits::{RenderContext, View, WidgetProps};
7use crate::{impl_props_builders, impl_styled_view};
8
9#[derive(Clone)]
11pub struct Tab {
12 pub label: String,
14}
15
16impl Tab {
17 pub fn new(label: impl Into<String>) -> Self {
19 Self {
20 label: label.into(),
21 }
22 }
23}
24
25pub struct Tabs {
27 tabs: Vec<Tab>,
28 selection: Selection,
29 fg: Option<Color>,
30 bg: Option<Color>,
31 active_fg: Option<Color>,
32 active_bg: Option<Color>,
33 divider: char,
34 props: WidgetProps,
35}
36
37impl Tabs {
38 pub fn new() -> Self {
40 Self {
41 tabs: Vec::new(),
42 selection: Selection::new(0),
43 fg: None,
44 bg: None,
45 active_fg: Some(Color::WHITE),
46 active_bg: Some(Color::BLUE),
47 divider: '│',
48 props: WidgetProps::new(),
49 }
50 }
51
52 pub fn tabs(mut self, tabs: Vec<impl Into<String>>) -> Self {
54 self.tabs = tabs.into_iter().map(|t| Tab::new(t)).collect();
55 self.selection.set_len(self.tabs.len());
56 self
57 }
58
59 pub fn tab(mut self, label: impl Into<String>) -> Self {
61 self.tabs.push(Tab::new(label));
62 self.selection.set_len(self.tabs.len());
63 self
64 }
65
66 pub fn selected(mut self, index: usize) -> Self {
68 self.selection.set(index);
69 self
70 }
71
72 pub fn fg(mut self, color: Color) -> Self {
74 self.fg = Some(color);
75 self
76 }
77
78 pub fn bg(mut self, color: Color) -> Self {
80 self.bg = Some(color);
81 self
82 }
83
84 pub fn active_style(mut self, fg: Color, bg: Color) -> Self {
86 self.active_fg = Some(fg);
87 self.active_bg = Some(bg);
88 self
89 }
90
91 pub fn divider(mut self, ch: char) -> Self {
93 self.divider = ch;
94 self
95 }
96
97 pub fn selected_index(&self) -> usize {
99 self.selection.index
100 }
101
102 pub fn selected_label(&self) -> Option<&str> {
104 self.tabs
105 .get(self.selection.index)
106 .map(|t| t.label.as_str())
107 }
108
109 pub fn select_next(&mut self) {
111 self.selection.next();
112 }
113
114 pub fn select_prev(&mut self) {
116 self.selection.prev();
117 }
118
119 pub fn select_first(&mut self) {
121 self.selection.first();
122 }
123
124 pub fn select_last(&mut self) {
126 self.selection.last();
127 }
128
129 pub fn select(&mut self, index: usize) {
131 self.selection.set(index);
132 }
133
134 pub fn handle_key(&mut self, key: &crate::event::Key) -> bool {
136 use crate::event::Key;
137
138 match key {
139 Key::Left | Key::Char('h') => {
140 let old = self.selection.index;
141 self.select_prev();
142 old != self.selection.index
143 }
144 Key::Right | Key::Char('l') => {
145 let old = self.selection.index;
146 self.select_next();
147 old != self.selection.index
148 }
149 Key::Home => {
150 let old = self.selection.index;
151 self.select_first();
152 old != self.selection.index
153 }
154 Key::End => {
155 let old = self.selection.index;
156 self.select_last();
157 old != self.selection.index
158 }
159 Key::Char(c) if c.is_ascii_digit() => {
160 let index = (*c as usize) - ('1' as usize);
161 if index < self.tabs.len() {
162 let old = self.selection.index;
163 self.selection.index = index;
164 old != self.selection.index
165 } else {
166 false
167 }
168 }
169 _ => false,
170 }
171 }
172
173 pub fn len(&self) -> usize {
175 self.tabs.len()
176 }
177
178 pub fn is_empty(&self) -> bool {
180 self.tabs.is_empty()
181 }
182}
183
184impl Default for Tabs {
185 fn default() -> Self {
186 Self::new()
187 }
188}
189
190impl View for Tabs {
191 fn render(&self, ctx: &mut RenderContext) {
192 let area = ctx.area;
193 if area.width < 3 || area.height < 1 || self.tabs.is_empty() {
194 return;
195 }
196
197 let mut x = area.x;
198
199 for (i, tab) in self.tabs.iter().enumerate() {
200 let is_active = i == self.selection.index;
201 let (fg, bg) = if is_active {
202 (self.active_fg, self.active_bg)
203 } else {
204 (self.fg, self.bg)
205 };
206
207 let mut cell = Cell::new(' ');
209 cell.fg = fg;
210 cell.bg = bg;
211 if x < area.x + area.width {
212 ctx.buffer.set(x, area.y, cell);
213 x += 1;
214 }
215
216 for ch in tab.label.chars() {
218 if x >= area.x + area.width {
219 break;
220 }
221 let mut cell = Cell::new(ch);
222 cell.fg = fg;
223 cell.bg = bg;
224 if is_active {
225 cell.modifier |= crate::render::Modifier::BOLD;
226 }
227 ctx.buffer.set(x, area.y, cell);
228 x += 1;
229 }
230
231 if x < area.x + area.width {
233 let mut cell = Cell::new(' ');
234 cell.fg = fg;
235 cell.bg = bg;
236 ctx.buffer.set(x, area.y, cell);
237 x += 1;
238 }
239
240 if i < self.tabs.len() - 1 && x < area.x + area.width {
242 let mut cell = Cell::new(self.divider);
243 cell.fg = self.fg;
244 ctx.buffer.set(x, area.y, cell);
245 x += 1;
246 }
247 }
248 }
249
250 crate::impl_view_meta!("Tabs");
251}
252
253pub fn tabs() -> Tabs {
255 Tabs::new()
256}
257
258impl_styled_view!(Tabs);
259impl_props_builders!(Tabs);
260
261