1use crate::_private::NonExhaustive;
5use crate::event::TabbedOutcome;
6use crate::tabbed::attached::AttachedTabs;
7use crate::tabbed::glued::GluedTabs;
8use rat_event::util::MouseFlagsN;
9use rat_event::{ct_event, flow, HandleEvent, MouseOnly, Regular};
10use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
11use rat_reloc::{relocate_area, relocate_areas, RelocatableState};
12use ratatui::buffer::Buffer;
13use ratatui::layout::Rect;
14use ratatui::style::Style;
15use ratatui::text::Line;
16use ratatui::widgets::{Block, StatefulWidget};
17use std::cmp::min;
18use std::fmt::Debug;
19
20mod attached;
21mod glued;
22
23#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
28pub enum TabPlacement {
29 #[default]
32 Top,
33 Left,
36 Right,
39 Bottom,
42}
43
44#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
46#[non_exhaustive]
47pub enum TabType {
48 Glued,
50
51 #[default]
59 Attached,
60}
61
62#[derive(Debug, Default)]
70pub struct Tabbed<'a> {
71 tab_type: TabType,
72 placement: TabPlacement,
73 closeable: bool,
74 tabs: Vec<Line<'a>>,
75 block: Option<Block<'a>>,
76
77 style: Style,
78 tab_style: Option<Style>,
79 select_style: Option<Style>,
80 focus_style: Option<Style>,
81}
82
83#[derive(Debug, Clone)]
85pub struct TabbedStyle {
86 pub style: Style,
87 pub tab: Option<Style>,
88 pub select: Option<Style>,
89 pub focus: Option<Style>,
90
91 pub tab_type: Option<TabType>,
92 pub placement: Option<TabPlacement>,
93 pub block: Option<Block<'static>>,
94
95 pub non_exhaustive: NonExhaustive,
96}
97
98#[derive(Debug, Default)]
100pub struct TabbedState {
101 pub area: Rect,
104 pub block_area: Rect,
107 pub widget_area: Rect,
111
112 pub tab_title_area: Rect,
115 pub tab_title_areas: Vec<Rect>,
118 pub tab_title_close_areas: Vec<Rect>,
121
122 pub selected: Option<usize>,
126
127 pub focus: FocusFlag,
130 pub mouse: MouseFlagsN,
133}
134
135pub(crate) mod event {
136 use rat_event::{ConsumedEvent, Outcome};
137
138 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
140 pub enum TabbedOutcome {
141 Continue,
143 Unchanged,
146 Changed,
151 Select(usize),
153 Close(usize),
155 }
156
157 impl ConsumedEvent for TabbedOutcome {
158 fn is_consumed(&self) -> bool {
159 *self != TabbedOutcome::Continue
160 }
161 }
162
163 impl From<bool> for TabbedOutcome {
165 fn from(value: bool) -> Self {
166 if value {
167 TabbedOutcome::Changed
168 } else {
169 TabbedOutcome::Unchanged
170 }
171 }
172 }
173
174 impl From<Outcome> for TabbedOutcome {
175 fn from(value: Outcome) -> Self {
176 match value {
177 Outcome::Continue => TabbedOutcome::Continue,
178 Outcome::Unchanged => TabbedOutcome::Unchanged,
179 Outcome::Changed => TabbedOutcome::Changed,
180 }
181 }
182 }
183
184 impl From<TabbedOutcome> for Outcome {
185 fn from(value: TabbedOutcome) -> Self {
186 match value {
187 TabbedOutcome::Continue => Outcome::Continue,
188 TabbedOutcome::Unchanged => Outcome::Unchanged,
189 TabbedOutcome::Changed => Outcome::Changed,
190 TabbedOutcome::Select(_) => Outcome::Changed,
191 TabbedOutcome::Close(_) => Outcome::Changed,
192 }
193 }
194 }
195}
196
197impl<'a> Tabbed<'a> {
198 pub fn new() -> Self {
199 Self::default()
200 }
201
202 pub fn tab_type(mut self, tab_type: TabType) -> Self {
204 self.tab_type = tab_type;
205 self
206 }
207
208 pub fn placement(mut self, placement: TabPlacement) -> Self {
210 self.placement = placement;
211 self
212 }
213
214 pub fn tabs(mut self, tabs: impl IntoIterator<Item = impl Into<Line<'a>>>) -> Self {
216 self.tabs = tabs.into_iter().map(|v| v.into()).collect::<Vec<_>>();
217 self
218 }
219
220 pub fn closeable(mut self, closeable: bool) -> Self {
224 self.closeable = closeable;
225 self
226 }
227
228 pub fn block(mut self, block: Block<'a>) -> Self {
230 self.block = Some(block);
231 self
232 }
233
234 pub fn styles(mut self, styles: TabbedStyle) -> Self {
236 self.style = styles.style;
237 if styles.tab.is_some() {
238 self.tab_style = styles.tab;
239 }
240 if styles.select.is_some() {
241 self.select_style = styles.select;
242 }
243 if styles.focus.is_some() {
244 self.focus_style = styles.focus;
245 }
246 if let Some(tab_type) = styles.tab_type {
247 self.tab_type = tab_type;
248 }
249 if let Some(placement) = styles.placement {
250 self.placement = placement
251 }
252 if styles.block.is_some() {
253 self.block = styles.block;
254 }
255 self
256 }
257
258 pub fn style(mut self, style: Style) -> Self {
260 self.style = style;
261 self
262 }
263
264 pub fn tab_style(mut self, style: Style) -> Self {
266 self.tab_style = Some(style);
267 self
268 }
269
270 pub fn select_style(mut self, style: Style) -> Self {
272 self.select_style = Some(style);
273 self
274 }
275
276 pub fn focus_style(mut self, style: Style) -> Self {
278 self.focus_style = Some(style);
279 self
280 }
281}
282
283impl Default for TabbedStyle {
284 fn default() -> Self {
285 Self {
286 style: Default::default(),
287 tab: None,
288 select: None,
289 focus: None,
290 tab_type: None,
291 placement: None,
292 block: None,
293 non_exhaustive: NonExhaustive,
294 }
295 }
296}
297
298impl StatefulWidget for Tabbed<'_> {
299 type State = TabbedState;
300
301 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
302 render_ref(&self, area, buf, state);
303 }
304}
305
306impl<'a> StatefulWidget for &Tabbed<'a> {
307 type State = TabbedState;
308
309 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
310 render_ref(self, area, buf, state);
311 }
312}
313
314fn render_ref(tabbed: &Tabbed<'_>, area: Rect, buf: &mut Buffer, state: &mut TabbedState) {
315 if tabbed.tabs.is_empty() {
316 state.selected = None;
317 } else {
318 if state.selected.is_none() {
319 state.selected = Some(0);
320 }
321 }
322
323 match tabbed.tab_type {
324 TabType::Glued => {
325 GluedTabs.layout(area, tabbed, state);
326 GluedTabs.render(buf, tabbed, state);
327 }
328 TabType::Attached => {
329 AttachedTabs.layout(area, tabbed, state);
330 AttachedTabs.render(buf, tabbed, state);
331 }
332 }
333}
334
335impl Clone for TabbedState {
336 fn clone(&self) -> Self {
337 Self {
338 area: self.area,
339 block_area: self.block_area,
340 widget_area: self.widget_area,
341 tab_title_area: self.tab_title_area,
342 tab_title_areas: self.tab_title_areas.clone(),
343 tab_title_close_areas: self.tab_title_close_areas.clone(),
344 selected: self.selected,
345 focus: FocusFlag::named(self.focus.name()),
346 mouse: Default::default(),
347 }
348 }
349}
350
351impl HasFocus for TabbedState {
352 fn build(&self, builder: &mut FocusBuilder) {
353 builder.leaf_widget(self);
354 }
355
356 fn focus(&self) -> FocusFlag {
357 self.focus.clone()
358 }
359
360 fn area(&self) -> Rect {
361 Rect::default()
362 }
363
364 fn navigable(&self) -> Navigation {
365 Navigation::Leave
366 }
367}
368
369impl RelocatableState for TabbedState {
370 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
371 self.area = relocate_area(self.area, shift, clip);
372 self.block_area = relocate_area(self.block_area, shift, clip);
373 self.widget_area = relocate_area(self.widget_area, shift, clip);
374 self.tab_title_area = relocate_area(self.tab_title_area, shift, clip);
375 relocate_areas(self.tab_title_areas.as_mut(), shift, clip);
376 }
377}
378
379impl TabbedState {
380 pub fn new() -> Self {
382 Default::default()
383 }
384
385 pub fn named(name: &str) -> Self {
387 Self {
388 focus: FocusFlag::named(name),
389 ..Default::default()
390 }
391 }
392
393 pub fn selected(&self) -> Option<usize> {
394 self.selected
395 }
396
397 pub fn select(&mut self, selected: Option<usize>) {
398 self.selected = selected;
399 }
400
401 pub fn next_tab(&mut self) -> bool {
403 let old_selected = self.selected;
404
405 if let Some(selected) = self.selected() {
406 self.selected = Some(min(
407 selected + 1,
408 self.tab_title_areas.len().saturating_sub(1),
409 ));
410 }
411
412 old_selected != self.selected
413 }
414
415 pub fn prev_tab(&mut self) -> bool {
417 let old_selected = self.selected;
418
419 if let Some(selected) = self.selected() {
420 if selected > 0 {
421 self.selected = Some(selected - 1);
422 }
423 }
424
425 old_selected != self.selected
426 }
427}
428
429impl HandleEvent<crossterm::event::Event, Regular, TabbedOutcome> for TabbedState {
431 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> TabbedOutcome {
432 if self.is_focused() {
433 flow!(match event {
434 ct_event!(keycode press Right) => self.next_tab().into(),
435 ct_event!(keycode press Left) => self.prev_tab().into(),
436 _ => TabbedOutcome::Continue,
437 });
438 }
439
440 self.handle(event, MouseOnly)
441 }
442}
443
444impl HandleEvent<crossterm::event::Event, MouseOnly, TabbedOutcome> for TabbedState {
445 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> TabbedOutcome {
446 match event {
447 ct_event!(mouse any for e) if self.mouse.hover(&self.tab_title_close_areas, e) => {
448 TabbedOutcome::Changed
449 }
450 ct_event!(mouse any for e) if self.mouse.drag(&[self.tab_title_area], e) => {
451 if let Some(n) = self.mouse.item_at(&self.tab_title_areas, e.column, e.row) {
452 self.select(Some(n));
453 TabbedOutcome::Select(n)
454 } else {
455 TabbedOutcome::Unchanged
456 }
457 }
458 ct_event!(mouse down Left for x, y)
459 if self.tab_title_area.contains((*x, *y).into()) =>
460 {
461 if let Some(sel) = self.mouse.item_at(&self.tab_title_close_areas, *x, *y) {
462 TabbedOutcome::Close(sel)
463 } else if let Some(sel) = self.mouse.item_at(&self.tab_title_areas, *x, *y) {
464 self.select(Some(sel));
465 TabbedOutcome::Select(sel)
466 } else {
467 TabbedOutcome::Continue
468 }
469 }
470
471 _ => TabbedOutcome::Continue,
472 }
473 }
474}
475
476trait TabWidget: Debug {
481 fn layout(
483 &self, area: Rect,
485 tabbed: &Tabbed<'_>,
486 state: &mut TabbedState,
487 );
488
489 fn render(
491 &self, buf: &mut Buffer,
493 tabbed: &Tabbed<'_>,
494 state: &mut TabbedState,
495 );
496}