1use crate::_private::NonExhaustive;
47use crate::event::TabbedOutcome;
48use crate::tabbed::attached::AttachedTabs;
49use crate::tabbed::glued::GluedTabs;
50use rat_event::util::MouseFlagsN;
51use rat_event::{HandleEvent, MouseOnly, Regular, ct_event, event_flow};
52use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
53use rat_reloc::{RelocatableState, relocate_area, relocate_areas};
54use ratatui::buffer::Buffer;
55use ratatui::layout::Rect;
56use ratatui::style::Style;
57use ratatui::text::Line;
58use ratatui::widgets::{Block, StatefulWidget};
59use std::cmp::min;
60use std::fmt::Debug;
61
62mod attached;
63mod glued;
64
65#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
70pub enum TabPlacement {
71 #[default]
74 Top,
75 Left,
78 Right,
81 Bottom,
84}
85
86#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
88#[non_exhaustive]
89pub enum TabType {
90 Glued,
92
93 #[default]
101 Attached,
102}
103
104#[derive(Debug, Default, Clone)]
112pub struct Tabbed<'a> {
113 tab_type: TabType,
114 placement: TabPlacement,
115 closeable: bool,
116 tabs: Vec<Line<'a>>,
117
118 style: Style,
119 block: Option<Block<'a>>,
120 tab_style: Option<Style>,
121 hover_style: Option<Style>,
122 select_style: Option<Style>,
123 focus_style: Option<Style>,
124}
125
126#[derive(Debug, Clone)]
128pub struct TabbedStyle {
129 pub style: Style,
130 pub block: Option<Block<'static>>,
131 pub border_style: Option<Style>,
132 pub title_style: Option<Style>,
133 pub tab: Option<Style>,
134 pub hover: Option<Style>,
135 pub select: Option<Style>,
136 pub focus: Option<Style>,
137
138 pub tab_type: Option<TabType>,
139 pub placement: Option<TabPlacement>,
140
141 pub non_exhaustive: NonExhaustive,
142}
143
144#[derive(Debug, Default)]
146pub struct TabbedState {
147 pub area: Rect,
150 pub block_area: Rect,
153 pub widget_area: Rect,
157
158 pub tab_title_area: Rect,
161 pub tab_title_areas: Vec<Rect>,
164 pub tab_title_close_areas: Vec<Rect>,
167
168 pub selected: Option<usize>,
172
173 pub focus: FocusFlag,
176 pub mouse: MouseFlagsN,
179}
180
181pub(crate) mod event {
182 use rat_event::{ConsumedEvent, Outcome};
183
184 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
186 pub enum TabbedOutcome {
187 Continue,
189 Unchanged,
192 Changed,
197 Select(usize),
199 Close(usize),
201 }
202
203 impl ConsumedEvent for TabbedOutcome {
204 fn is_consumed(&self) -> bool {
205 *self != TabbedOutcome::Continue
206 }
207 }
208
209 impl From<bool> for TabbedOutcome {
211 fn from(value: bool) -> Self {
212 if value {
213 TabbedOutcome::Changed
214 } else {
215 TabbedOutcome::Unchanged
216 }
217 }
218 }
219
220 impl From<Outcome> for TabbedOutcome {
221 fn from(value: Outcome) -> Self {
222 match value {
223 Outcome::Continue => TabbedOutcome::Continue,
224 Outcome::Unchanged => TabbedOutcome::Unchanged,
225 Outcome::Changed => TabbedOutcome::Changed,
226 }
227 }
228 }
229
230 impl From<TabbedOutcome> for Outcome {
231 fn from(value: TabbedOutcome) -> Self {
232 match value {
233 TabbedOutcome::Continue => Outcome::Continue,
234 TabbedOutcome::Unchanged => Outcome::Unchanged,
235 TabbedOutcome::Changed => Outcome::Changed,
236 TabbedOutcome::Select(_) => Outcome::Changed,
237 TabbedOutcome::Close(_) => Outcome::Changed,
238 }
239 }
240 }
241}
242
243impl<'a> Tabbed<'a> {
244 pub fn new() -> Self {
245 Self::default()
246 }
247
248 pub fn tab_type(mut self, tab_type: TabType) -> Self {
250 self.tab_type = tab_type;
251 self
252 }
253
254 pub fn placement(mut self, placement: TabPlacement) -> Self {
256 self.placement = placement;
257 self
258 }
259
260 pub fn tabs(mut self, tabs: impl IntoIterator<Item = impl Into<Line<'a>>>) -> Self {
262 self.tabs = tabs.into_iter().map(|v| v.into()).collect::<Vec<_>>();
263 self
264 }
265
266 pub fn closeable(mut self, closeable: bool) -> Self {
270 self.closeable = closeable;
271 self
272 }
273
274 pub fn block(mut self, block: Block<'a>) -> Self {
276 self.block = Some(block);
277 self
278 }
279
280 pub fn styles(mut self, styles: TabbedStyle) -> Self {
282 self.style = styles.style;
283 if styles.block.is_some() {
284 self.block = styles.block;
285 }
286 if let Some(border_style) = styles.border_style {
287 self.block = self.block.map(|v| v.border_style(border_style));
288 }
289 if let Some(title_style) = styles.title_style {
290 self.block = self.block.map(|v| v.title_style(title_style));
291 }
292 self.block = self.block.map(|v| v.style(self.style));
293
294 if styles.tab.is_some() {
295 self.tab_style = styles.tab;
296 }
297 if styles.select.is_some() {
298 self.select_style = styles.select;
299 }
300 if styles.hover.is_some() {
301 self.hover_style = styles.hover;
302 }
303 if styles.focus.is_some() {
304 self.focus_style = styles.focus;
305 }
306 if let Some(tab_type) = styles.tab_type {
307 self.tab_type = tab_type;
308 }
309 if let Some(placement) = styles.placement {
310 self.placement = placement
311 }
312 self
313 }
314
315 pub fn style(mut self, style: Style) -> Self {
317 self.style = style;
318 self.block = self.block.map(|v| v.style(style));
319 self
320 }
321
322 pub fn tab_style(mut self, style: Style) -> Self {
324 self.tab_style = Some(style);
325 self
326 }
327
328 pub fn hover_style(mut self, style: Style) -> Self {
330 self.hover_style = Some(style);
331 self
332 }
333
334 pub fn select_style(mut self, style: Style) -> Self {
336 self.select_style = Some(style);
337 self
338 }
339
340 pub fn focus_style(mut self, style: Style) -> Self {
342 self.focus_style = Some(style);
343 self
344 }
345}
346
347impl Default for TabbedStyle {
348 fn default() -> Self {
349 Self {
350 style: Default::default(),
351 tab: None,
352 hover: None,
353 select: None,
354 focus: None,
355 tab_type: None,
356 placement: None,
357 block: None,
358 border_style: None,
359 title_style: None,
360 non_exhaustive: NonExhaustive,
361 }
362 }
363}
364
365impl StatefulWidget for Tabbed<'_> {
366 type State = TabbedState;
367
368 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
369 render_ref(&self, area, buf, state);
370 }
371}
372
373impl<'a> StatefulWidget for &Tabbed<'a> {
374 type State = TabbedState;
375
376 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
377 render_ref(self, area, buf, state);
378 }
379}
380
381fn render_ref(tabbed: &Tabbed<'_>, area: Rect, buf: &mut Buffer, state: &mut TabbedState) {
382 if tabbed.tabs.is_empty() {
383 state.selected = None;
384 } else {
385 if state.selected.is_none() {
386 state.selected = Some(0);
387 }
388 }
389
390 match tabbed.tab_type {
391 TabType::Glued => {
392 GluedTabs.layout(area, tabbed, state);
393 GluedTabs.render(buf, tabbed, state);
394 }
395 TabType::Attached => {
396 AttachedTabs.layout(area, tabbed, state);
397 AttachedTabs.render(buf, tabbed, state);
398 }
399 }
400}
401
402impl Clone for TabbedState {
403 fn clone(&self) -> Self {
404 Self {
405 area: self.area,
406 block_area: self.block_area,
407 widget_area: self.widget_area,
408 tab_title_area: self.tab_title_area,
409 tab_title_areas: self.tab_title_areas.clone(),
410 tab_title_close_areas: self.tab_title_close_areas.clone(),
411 selected: self.selected,
412 focus: self.focus.new_instance(),
413 mouse: Default::default(),
414 }
415 }
416}
417
418impl HasFocus for TabbedState {
419 fn build(&self, builder: &mut FocusBuilder) {
420 builder.leaf_widget(self);
421 }
422
423 fn focus(&self) -> FocusFlag {
424 self.focus.clone()
425 }
426
427 fn area(&self) -> Rect {
428 Rect::default()
429 }
430
431 fn navigable(&self) -> Navigation {
432 Navigation::Leave
433 }
434}
435
436impl RelocatableState for TabbedState {
437 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
438 self.area = relocate_area(self.area, shift, clip);
439 self.block_area = relocate_area(self.block_area, shift, clip);
440 self.widget_area = relocate_area(self.widget_area, shift, clip);
441 self.tab_title_area = relocate_area(self.tab_title_area, shift, clip);
442 relocate_areas(self.tab_title_areas.as_mut(), shift, clip);
443 relocate_areas(self.tab_title_close_areas.as_mut(), shift, clip);
444 }
445}
446
447impl TabbedState {
448 pub fn new() -> Self {
450 Default::default()
451 }
452
453 pub fn named(name: &str) -> Self {
455 let mut z = Self::default();
456 z.focus = z.focus.with_name(name);
457 z
458 }
459
460 pub fn selected(&self) -> Option<usize> {
461 self.selected
462 }
463
464 pub fn select(&mut self, selected: Option<usize>) {
465 self.selected = selected;
466 }
467
468 pub fn next_tab(&mut self) -> bool {
470 let old_selected = self.selected;
471
472 if let Some(selected) = self.selected() {
473 self.selected = Some(min(
474 selected + 1,
475 self.tab_title_areas.len().saturating_sub(1),
476 ));
477 }
478
479 old_selected != self.selected
480 }
481
482 pub fn prev_tab(&mut self) -> bool {
484 let old_selected = self.selected;
485
486 if let Some(selected) = self.selected() {
487 if selected > 0 {
488 self.selected = Some(selected - 1);
489 }
490 }
491
492 old_selected != self.selected
493 }
494}
495
496impl HandleEvent<crossterm::event::Event, Regular, TabbedOutcome> for TabbedState {
498 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> TabbedOutcome {
499 if self.is_focused() {
500 event_flow!(
501 return match event {
502 ct_event!(keycode press Left) => self.prev_tab().into(),
503 ct_event!(keycode press Right) => self.next_tab().into(),
504 ct_event!(keycode press Up) => self.prev_tab().into(),
505 ct_event!(keycode press Down) => self.next_tab().into(),
506 _ => TabbedOutcome::Continue,
507 }
508 );
509 }
510
511 self.handle(event, MouseOnly)
512 }
513}
514
515impl HandleEvent<crossterm::event::Event, MouseOnly, TabbedOutcome> for TabbedState {
516 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> TabbedOutcome {
517 match event {
518 ct_event!(mouse any for e) if self.mouse.hover(&self.tab_title_close_areas, e) => {
519 TabbedOutcome::Changed
520 }
521 ct_event!(mouse any for e) if self.mouse.drag(&[self.tab_title_area], e) => {
522 if let Some(n) = self.mouse.item_at(&self.tab_title_areas, e.column, e.row) {
523 self.select(Some(n));
524 TabbedOutcome::Select(n)
525 } else {
526 TabbedOutcome::Unchanged
527 }
528 }
529 ct_event!(mouse down Left for x, y)
530 if self.tab_title_area.contains((*x, *y).into()) =>
531 {
532 if let Some(sel) = self.mouse.item_at(&self.tab_title_close_areas, *x, *y) {
533 TabbedOutcome::Close(sel)
534 } else if let Some(sel) = self.mouse.item_at(&self.tab_title_areas, *x, *y) {
535 self.select(Some(sel));
536 TabbedOutcome::Select(sel)
537 } else {
538 TabbedOutcome::Continue
539 }
540 }
541
542 _ => TabbedOutcome::Continue,
543 }
544 }
545}
546
547trait TabWidget: Debug {
552 fn layout(
554 &self, area: Rect,
556 tabbed: &Tabbed<'_>,
557 state: &mut TabbedState,
558 );
559
560 fn render(
562 &self, buf: &mut Buffer,
564 tabbed: &Tabbed<'_>,
565 state: &mut TabbedState,
566 );
567}
568
569pub fn handle_events(
573 state: &mut TabbedState,
574 focus: bool,
575 event: &crossterm::event::Event,
576) -> TabbedOutcome {
577 state.focus.set(focus);
578 HandleEvent::handle(state, event, Regular)
579}
580
581pub fn handle_mouse_events(
583 state: &mut TabbedState,
584 event: &crossterm::event::Event,
585) -> TabbedOutcome {
586 HandleEvent::handle(state, event, MouseOnly)
587}