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