1use crate::_private::NonExhaustive;
49use crate::event::TabbedOutcome;
50use crate::tabbed::attached::AttachedTabs;
51use crate::tabbed::glued::GluedTabs;
52use crate::util::union_all_non_empty;
53use rat_event::util::MouseFlagsN;
54use rat_event::{HandleEvent, MouseOnly, Regular, ct_event, event_flow};
55use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
56use rat_reloc::{RelocatableState, relocate_area, relocate_areas};
57use ratatui_core::buffer::Buffer;
58use ratatui_core::layout::Rect;
59use ratatui_core::style::Style;
60use ratatui_core::text::Line;
61use ratatui_core::widgets::StatefulWidget;
62use ratatui_crossterm::crossterm::event::Event;
63use ratatui_widgets::block::Block;
64use std::cmp::min;
65use std::fmt::Debug;
66use std::rc::Rc;
67
68mod attached;
69pub(crate) mod event;
70mod glued;
71
72#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
77pub enum TabPlacement {
78 #[default]
81 Top,
82 Left,
85 Right,
88 Bottom,
91}
92
93#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
95#[non_exhaustive]
96pub enum TabType {
97 Glued,
99
100 #[default]
108 Attached,
109}
110
111#[derive(Debug, Default, Clone)]
119pub struct Tabbed<'a> {
120 tab_type: TabType,
121 placement: TabPlacement,
122 closeable: bool,
123 tabs: Vec<Line<'a>>,
124
125 style: Style,
126 block: Option<Block<'a>>,
127 tab_style: Option<Style>,
128 hover_style: Option<Style>,
129 select_style: Option<Style>,
130 focus_style: Option<Style>,
131}
132
133#[derive(Debug, Clone)]
135pub struct LayoutWidget<'a> {
136 tab: Rc<Tabbed<'a>>,
137}
138
139#[derive(Debug, Clone)]
141pub struct TabbedWidget<'a> {
142 tab: Rc<Tabbed<'a>>,
143}
144
145#[derive(Debug, Clone)]
147pub struct TabbedStyle {
148 pub style: Style,
149 pub block: Option<Block<'static>>,
150 pub border_style: Option<Style>,
151 pub title_style: Option<Style>,
152 pub tab: Option<Style>,
153 pub hover: Option<Style>,
154 pub select: Option<Style>,
155 pub focus: Option<Style>,
156
157 pub tab_type: Option<TabType>,
158 pub placement: Option<TabPlacement>,
159
160 pub non_exhaustive: NonExhaustive,
161}
162
163#[derive(Debug)]
165pub struct TabbedState {
166 pub area: Rect,
169 pub block_area: Rect,
172 pub widget_area: Rect,
176
177 pub tab_title_area: Rect,
180 pub tab_title_areas: Vec<Rect>,
183 pub tab_title_close_areas: Vec<Rect>,
186
187 pub selected: Option<usize>,
191
192 pub focus: FocusFlag,
195 pub mouse: MouseFlagsN,
198
199 relocate_popup: bool,
202
203 pub non_exhaustive: NonExhaustive,
204}
205
206impl<'a> Tabbed<'a> {
207 pub fn new() -> Self {
208 Self::default()
209 }
210
211 pub fn tab_type(mut self, tab_type: TabType) -> Self {
213 self.tab_type = tab_type;
214 self
215 }
216
217 pub fn placement(mut self, placement: TabPlacement) -> Self {
219 self.placement = placement;
220 self
221 }
222
223 pub fn tabs(mut self, tabs: impl IntoIterator<Item = impl Into<Line<'a>>>) -> Self {
225 self.tabs = tabs.into_iter().map(|v| v.into()).collect::<Vec<_>>();
226 self
227 }
228
229 pub fn closeable(mut self, closeable: bool) -> Self {
233 self.closeable = closeable;
234 self
235 }
236
237 pub fn block(mut self, block: Block<'a>) -> Self {
239 self.block = Some(block);
240 self
241 }
242
243 pub fn styles(mut self, styles: TabbedStyle) -> Self {
245 self.style = styles.style;
246 if styles.block.is_some() {
247 self.block = styles.block;
248 }
249 if let Some(border_style) = styles.border_style {
250 self.block = self.block.map(|v| v.border_style(border_style));
251 }
252 if let Some(title_style) = styles.title_style {
253 self.block = self.block.map(|v| v.title_style(title_style));
254 }
255 self.block = self.block.map(|v| v.style(self.style));
256
257 if styles.tab.is_some() {
258 self.tab_style = styles.tab;
259 }
260 if styles.select.is_some() {
261 self.select_style = styles.select;
262 }
263 if styles.hover.is_some() {
264 self.hover_style = styles.hover;
265 }
266 if styles.focus.is_some() {
267 self.focus_style = styles.focus;
268 }
269 if let Some(tab_type) = styles.tab_type {
270 self.tab_type = tab_type;
271 }
272 if let Some(placement) = styles.placement {
273 self.placement = placement
274 }
275 self
276 }
277
278 pub fn style(mut self, style: Style) -> Self {
280 self.style = style;
281 self.block = self.block.map(|v| v.style(style));
282 self
283 }
284
285 pub fn tab_style(mut self, style: Style) -> Self {
287 self.tab_style = Some(style);
288 self
289 }
290
291 pub fn hover_style(mut self, style: Style) -> Self {
293 self.hover_style = Some(style);
294 self
295 }
296
297 pub fn select_style(mut self, style: Style) -> Self {
299 self.select_style = Some(style);
300 self
301 }
302
303 pub fn focus_style(mut self, style: Style) -> Self {
305 self.focus_style = Some(style);
306 self
307 }
308
309 pub fn into_widgets(self) -> (LayoutWidget<'a>, TabbedWidget<'a>) {
320 let rc = Rc::new(self);
321 (
322 LayoutWidget {
323 tab: rc.clone(), },
325 TabbedWidget {
326 tab:rc, },
328 )
329 }
330}
331
332impl Default for TabbedStyle {
333 fn default() -> Self {
334 Self {
335 style: Default::default(),
336 tab: None,
337 hover: None,
338 select: None,
339 focus: None,
340 tab_type: None,
341 placement: None,
342 block: None,
343 border_style: None,
344 title_style: None,
345 non_exhaustive: NonExhaustive,
346 }
347 }
348}
349
350impl StatefulWidget for &Tabbed<'_> {
351 type State = TabbedState;
352
353 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
354 layout(self, area, state);
355 render(self, buf, state);
356 state.relocate_popup = false;
357 }
358}
359
360impl StatefulWidget for Tabbed<'_> {
361 type State = TabbedState;
362
363 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
364 layout(&self, area, state);
365 render(&self, buf, state);
366 state.relocate_popup = false;
367 }
368}
369
370impl<'a> StatefulWidget for &LayoutWidget<'a> {
371 type State = TabbedState;
372
373 fn render(self, area: Rect, _buf: &mut Buffer, state: &mut Self::State) {
374 layout(self.tab.as_ref(), area, state);
375 }
376}
377
378impl<'a> StatefulWidget for LayoutWidget<'a> {
379 type State = TabbedState;
380
381 fn render(self, area: Rect, _buf: &mut Buffer, state: &mut Self::State) {
382 layout(self.tab.as_ref(), area, state);
383 }
384}
385
386fn layout(tabbed: &Tabbed<'_>, area: Rect, state: &mut TabbedState) {
387 state.relocate_popup = true;
388 if tabbed.tabs.is_empty() {
389 state.selected = None;
390 } else {
391 if state.selected.is_none() {
392 state.selected = Some(0);
393 }
394 }
395
396 match tabbed.tab_type {
397 TabType::Glued => {
398 GluedTabs.layout(area, tabbed, state);
399 }
400 TabType::Attached => {
401 AttachedTabs.layout(area, tabbed, state);
402 }
403 }
404}
405
406impl<'a> StatefulWidget for &TabbedWidget<'a> {
407 type State = TabbedState;
408
409 fn render(self, _area: Rect, buf: &mut Buffer, state: &mut Self::State) {
410 render(self.tab.as_ref(), buf, state);
411 }
412}
413
414impl<'a> StatefulWidget for TabbedWidget<'a> {
415 type State = TabbedState;
416
417 fn render(self, _area: Rect, buf: &mut Buffer, state: &mut Self::State) {
418 render(self.tab.as_ref(), buf, state);
419 }
420}
421
422fn render(tabbed: &Tabbed<'_>, buf: &mut Buffer, state: &mut TabbedState) {
423 if tabbed.tabs.is_empty() {
424 state.selected = None;
425 } else {
426 if state.selected.is_none() {
427 state.selected = Some(0);
428 }
429 }
430
431 match tabbed.tab_type {
432 TabType::Glued => {
433 GluedTabs.render(buf, tabbed, state);
434 }
435 TabType::Attached => {
436 AttachedTabs.render(buf, tabbed, state);
437 }
438 }
439}
440
441impl Default for TabbedState {
442 fn default() -> Self {
443 Self {
444 area: Default::default(),
445 block_area: Default::default(),
446 widget_area: Default::default(),
447 tab_title_area: Default::default(),
448 tab_title_areas: Default::default(),
449 tab_title_close_areas: Default::default(),
450 selected: Default::default(),
451 focus: Default::default(),
452 mouse: Default::default(),
453 relocate_popup: Default::default(),
454 non_exhaustive: NonExhaustive,
455 }
456 }
457}
458
459impl Clone for TabbedState {
460 fn clone(&self) -> Self {
461 Self {
462 area: self.area,
463 block_area: self.block_area,
464 widget_area: self.widget_area,
465 tab_title_area: self.tab_title_area,
466 tab_title_areas: self.tab_title_areas.clone(),
467 tab_title_close_areas: self.tab_title_close_areas.clone(),
468 selected: self.selected,
469 focus: self.focus.new_instance(),
470 mouse: Default::default(),
471 relocate_popup: self.relocate_popup,
472 non_exhaustive: NonExhaustive,
473 }
474 }
475}
476
477impl HasFocus for TabbedState {
478 fn build(&self, builder: &mut FocusBuilder) {
479 builder.leaf_widget(self);
480 }
481
482 fn build_nav(&self, navigable: Navigation, builder: &mut FocusBuilder) {
483 if !matches!(navigable, Navigation::None | Navigation::Leave) {
484 builder.widget_with_flags(
485 self.focus(),
486 union_all_non_empty(&self.tab_title_areas),
487 self.area_z(),
488 navigable,
489 );
490 } else {
491 self.build(builder);
492 }
493 }
494
495 fn focus(&self) -> FocusFlag {
496 self.focus.clone()
497 }
498
499 fn area(&self) -> Rect {
500 Rect::default()
501 }
502
503 fn navigable(&self) -> Navigation {
504 Navigation::Leave
505 }
506}
507
508impl RelocatableState for TabbedState {
509 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
510 if !self.relocate_popup {
511 self.area = relocate_area(self.area, shift, clip);
512 self.block_area = relocate_area(self.block_area, shift, clip);
513 self.widget_area = relocate_area(self.widget_area, shift, clip);
514 self.tab_title_area = relocate_area(self.tab_title_area, shift, clip);
515 relocate_areas(self.tab_title_areas.as_mut(), shift, clip);
516 relocate_areas(self.tab_title_close_areas.as_mut(), shift, clip);
517 }
518 }
519
520 fn relocate_popup(&mut self, shift: (i16, i16), clip: Rect) {
521 if self.relocate_popup {
522 self.relocate_popup = false;
523 self.area = relocate_area(self.area, shift, clip);
524 self.block_area = relocate_area(self.block_area, shift, clip);
525 self.widget_area = relocate_area(self.widget_area, shift, clip);
526 self.tab_title_area = relocate_area(self.tab_title_area, shift, clip);
527 relocate_areas(self.tab_title_areas.as_mut(), shift, clip);
528 relocate_areas(self.tab_title_close_areas.as_mut(), shift, clip);
529 }
530 }
531}
532
533impl TabbedState {
534 pub fn new() -> Self {
536 Default::default()
537 }
538
539 pub fn named(name: &str) -> Self {
541 let mut z = Self::default();
542 z.focus = z.focus.with_name(name);
543 z
544 }
545
546 pub fn selected(&self) -> Option<usize> {
547 self.selected
548 }
549
550 pub fn select(&mut self, selected: Option<usize>) {
551 self.selected = selected;
552 }
553
554 pub fn next_tab(&mut self) -> bool {
556 let old_selected = self.selected;
557
558 if let Some(selected) = self.selected() {
559 self.selected = Some(min(
560 selected + 1,
561 self.tab_title_areas.len().saturating_sub(1),
562 ));
563 }
564
565 old_selected != self.selected
566 }
567
568 pub fn prev_tab(&mut self) -> bool {
570 let old_selected = self.selected;
571
572 if let Some(selected) = self.selected() {
573 if selected > 0 {
574 self.selected = Some(selected - 1);
575 }
576 }
577
578 old_selected != self.selected
579 }
580}
581
582impl HandleEvent<Event, Regular, TabbedOutcome> for TabbedState {
584 fn handle(&mut self, event: &Event, _qualifier: Regular) -> TabbedOutcome {
585 if self.is_focused() {
586 event_flow!(
587 return match event {
588 ct_event!(keycode press Left) => self.prev_tab().into(),
589 ct_event!(keycode press Right) => self.next_tab().into(),
590 ct_event!(keycode press Up) => self.prev_tab().into(),
591 ct_event!(keycode press Down) => self.next_tab().into(),
592 _ => TabbedOutcome::Continue,
593 }
594 );
595 }
596
597 self.handle(event, MouseOnly)
598 }
599}
600
601impl HandleEvent<Event, MouseOnly, TabbedOutcome> for TabbedState {
602 fn handle(&mut self, event: &Event, _qualifier: MouseOnly) -> TabbedOutcome {
603 match event {
604 ct_event!(mouse any for e) if self.mouse.hover(&self.tab_title_close_areas, e) => {
605 TabbedOutcome::Changed
606 }
607 ct_event!(mouse any for e) if self.mouse.drag(&[self.tab_title_area], e) => {
608 if let Some(n) = self.mouse.item_at(&self.tab_title_areas, e.column, e.row) {
609 self.select(Some(n));
610 TabbedOutcome::Select(n)
611 } else {
612 TabbedOutcome::Unchanged
613 }
614 }
615 ct_event!(mouse down Left for x, y)
616 if self.tab_title_area.contains((*x, *y).into()) =>
617 {
618 if let Some(sel) = self.mouse.item_at(&self.tab_title_close_areas, *x, *y) {
619 TabbedOutcome::Close(sel)
620 } else if let Some(sel) = self.mouse.item_at(&self.tab_title_areas, *x, *y) {
621 self.select(Some(sel));
622 TabbedOutcome::Select(sel)
623 } else {
624 TabbedOutcome::Continue
625 }
626 }
627
628 _ => TabbedOutcome::Continue,
629 }
630 }
631}
632
633trait TabWidget: Debug {
638 fn layout(
640 &self, area: Rect,
642 tabbed: &Tabbed<'_>,
643 state: &mut TabbedState,
644 );
645
646 fn render(
648 &self, buf: &mut Buffer,
650 tabbed: &Tabbed<'_>,
651 state: &mut TabbedState,
652 );
653}
654
655pub fn handle_events(state: &mut TabbedState, focus: bool, event: &Event) -> TabbedOutcome {
659 state.focus.set(focus);
660 HandleEvent::handle(state, event, Regular)
661}
662
663pub fn handle_mouse_events(state: &mut TabbedState, event: &Event) -> TabbedOutcome {
665 HandleEvent::handle(state, event, MouseOnly)
666}