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, 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)]
112pub struct Tabbed<'a> {
113 tab_type: TabType,
114 placement: TabPlacement,
115 closeable: bool,
116 tabs: Vec<Line<'a>>,
117 block: Option<Block<'a>>,
118
119 style: Style,
120 tab_style: Option<Style>,
121 select_style: Option<Style>,
122 focus_style: Option<Style>,
123}
124
125#[derive(Debug, Clone)]
127pub struct TabbedStyle {
128 pub style: Style,
129 pub tab: Option<Style>,
130 pub select: Option<Style>,
131 pub focus: Option<Style>,
132
133 pub tab_type: Option<TabType>,
134 pub placement: Option<TabPlacement>,
135 pub block: Option<Block<'static>>,
136
137 pub non_exhaustive: NonExhaustive,
138}
139
140#[derive(Debug, Default)]
142pub struct TabbedState {
143 pub area: Rect,
146 pub block_area: Rect,
149 pub widget_area: Rect,
154
155 pub tab_title_area: Rect,
158 pub tab_title_areas: Vec<Rect>,
161 pub tab_title_close_areas: Vec<Rect>,
164
165 pub selected: Option<usize>,
169
170 pub focus: FocusFlag,
173 pub mouse: MouseFlagsN,
176}
177
178pub(crate) mod event {
179 use rat_event::{ConsumedEvent, Outcome};
180
181 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
183 pub enum TabbedOutcome {
184 Continue,
186 Unchanged,
189 Changed,
194 Select(usize),
196 Close(usize),
198 }
199
200 impl ConsumedEvent for TabbedOutcome {
201 fn is_consumed(&self) -> bool {
202 *self != TabbedOutcome::Continue
203 }
204 }
205
206 impl From<bool> for TabbedOutcome {
208 fn from(value: bool) -> Self {
209 if value {
210 TabbedOutcome::Changed
211 } else {
212 TabbedOutcome::Unchanged
213 }
214 }
215 }
216
217 impl From<Outcome> for TabbedOutcome {
218 fn from(value: Outcome) -> Self {
219 match value {
220 Outcome::Continue => TabbedOutcome::Continue,
221 Outcome::Unchanged => TabbedOutcome::Unchanged,
222 Outcome::Changed => TabbedOutcome::Changed,
223 }
224 }
225 }
226
227 impl From<TabbedOutcome> for Outcome {
228 fn from(value: TabbedOutcome) -> Self {
229 match value {
230 TabbedOutcome::Continue => Outcome::Continue,
231 TabbedOutcome::Unchanged => Outcome::Unchanged,
232 TabbedOutcome::Changed => Outcome::Changed,
233 TabbedOutcome::Select(_) => Outcome::Changed,
234 TabbedOutcome::Close(_) => Outcome::Changed,
235 }
236 }
237 }
238}
239
240impl<'a> Tabbed<'a> {
241 pub fn new() -> Self {
242 Self::default()
243 }
244
245 pub fn tab_type(mut self, tab_type: TabType) -> Self {
247 self.tab_type = tab_type;
248 self
249 }
250
251 pub fn placement(mut self, placement: TabPlacement) -> Self {
253 self.placement = placement;
254 self
255 }
256
257 pub fn tabs(mut self, tabs: impl IntoIterator<Item = impl Into<Line<'a>>>) -> Self {
259 self.tabs = tabs.into_iter().map(|v| v.into()).collect::<Vec<_>>();
260 self
261 }
262
263 pub fn closeable(mut self, closeable: bool) -> Self {
267 self.closeable = closeable;
268 self
269 }
270
271 pub fn block(mut self, block: Block<'a>) -> Self {
273 self.block = Some(block);
274 self
275 }
276
277 pub fn styles(mut self, styles: TabbedStyle) -> Self {
279 self.style = styles.style;
280 if styles.tab.is_some() {
281 self.tab_style = styles.tab;
282 }
283 if styles.select.is_some() {
284 self.select_style = styles.select;
285 }
286 if styles.focus.is_some() {
287 self.focus_style = styles.focus;
288 }
289 if let Some(tab_type) = styles.tab_type {
290 self.tab_type = tab_type;
291 }
292 if let Some(placement) = styles.placement {
293 self.placement = placement
294 }
295 if styles.block.is_some() {
296 self.block = styles.block;
297 }
298 self
299 }
300
301 pub fn style(mut self, style: Style) -> Self {
303 self.style = style;
304 self
305 }
306
307 pub fn tab_style(mut self, style: Style) -> Self {
309 self.tab_style = Some(style);
310 self
311 }
312
313 pub fn select_style(mut self, style: Style) -> Self {
315 self.select_style = Some(style);
316 self
317 }
318
319 pub fn focus_style(mut self, style: Style) -> Self {
321 self.focus_style = Some(style);
322 self
323 }
324}
325
326impl Default for TabbedStyle {
327 fn default() -> Self {
328 Self {
329 style: Default::default(),
330 tab: None,
331 select: None,
332 focus: None,
333 tab_type: None,
334 placement: None,
335 block: None,
336 non_exhaustive: NonExhaustive,
337 }
338 }
339}
340
341impl StatefulWidget for Tabbed<'_> {
342 type State = TabbedState;
343
344 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
345 render_ref(&self, area, buf, state);
346 }
347}
348
349impl<'a> StatefulWidget for &Tabbed<'a> {
350 type State = TabbedState;
351
352 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
353 render_ref(self, area, buf, state);
354 }
355}
356
357fn render_ref(tabbed: &Tabbed<'_>, area: Rect, buf: &mut Buffer, state: &mut TabbedState) {
358 if tabbed.tabs.is_empty() {
359 state.selected = None;
360 } else {
361 if state.selected.is_none() {
362 state.selected = Some(0);
363 }
364 }
365
366 match tabbed.tab_type {
367 TabType::Glued => {
368 GluedTabs.layout(area, tabbed, state);
369 GluedTabs.render(buf, tabbed, state);
370 }
371 TabType::Attached => {
372 AttachedTabs.layout(area, tabbed, state);
373 AttachedTabs.render(buf, tabbed, state);
374 }
375 }
376}
377
378impl Clone for TabbedState {
379 fn clone(&self) -> Self {
380 Self {
381 area: self.area,
382 block_area: self.block_area,
383 widget_area: self.widget_area,
384 tab_title_area: self.tab_title_area,
385 tab_title_areas: self.tab_title_areas.clone(),
386 tab_title_close_areas: self.tab_title_close_areas.clone(),
387 selected: self.selected,
388 focus: FocusFlag::named(self.focus.name()),
389 mouse: Default::default(),
390 }
391 }
392}
393
394impl HasFocus for TabbedState {
395 fn build(&self, builder: &mut FocusBuilder) {
396 builder.leaf_widget(self);
397 }
398
399 fn focus(&self) -> FocusFlag {
400 self.focus.clone()
401 }
402
403 fn area(&self) -> Rect {
404 Rect::default()
405 }
406
407 fn navigable(&self) -> Navigation {
408 Navigation::Leave
409 }
410}
411
412impl RelocatableState for TabbedState {
413 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
414 self.area = relocate_area(self.area, shift, clip);
415 self.block_area = relocate_area(self.block_area, shift, clip);
416 self.widget_area = relocate_area(self.widget_area, shift, clip);
417 self.tab_title_area = relocate_area(self.tab_title_area, shift, clip);
418 relocate_areas(self.tab_title_areas.as_mut(), shift, clip);
419 }
420}
421
422impl TabbedState {
423 pub fn new() -> Self {
425 Default::default()
426 }
427
428 pub fn named(name: &str) -> Self {
430 Self {
431 focus: FocusFlag::named(name),
432 ..Default::default()
433 }
434 }
435
436 pub fn selected(&self) -> Option<usize> {
437 self.selected
438 }
439
440 pub fn select(&mut self, selected: Option<usize>) {
441 self.selected = selected;
442 }
443
444 pub fn next_tab(&mut self) -> bool {
446 let old_selected = self.selected;
447
448 if let Some(selected) = self.selected() {
449 self.selected = Some(min(
450 selected + 1,
451 self.tab_title_areas.len().saturating_sub(1),
452 ));
453 }
454
455 old_selected != self.selected
456 }
457
458 pub fn prev_tab(&mut self) -> bool {
460 let old_selected = self.selected;
461
462 if let Some(selected) = self.selected() {
463 if selected > 0 {
464 self.selected = Some(selected - 1);
465 }
466 }
467
468 old_selected != self.selected
469 }
470}
471
472impl HandleEvent<crossterm::event::Event, Regular, TabbedOutcome> for TabbedState {
474 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> TabbedOutcome {
475 if self.is_focused() {
476 flow!(match event {
477 ct_event!(keycode press Left) => self.prev_tab().into(),
478 ct_event!(keycode press Right) => self.next_tab().into(),
479 ct_event!(keycode press Up) => self.prev_tab().into(),
480 ct_event!(keycode press Down) => self.next_tab().into(),
481 _ => TabbedOutcome::Continue,
482 });
483 }
484
485 self.handle(event, MouseOnly)
486 }
487}
488
489impl HandleEvent<crossterm::event::Event, MouseOnly, TabbedOutcome> for TabbedState {
490 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> TabbedOutcome {
491 match event {
492 ct_event!(mouse any for e) if self.mouse.hover(&self.tab_title_close_areas, e) => {
493 TabbedOutcome::Changed
494 }
495 ct_event!(mouse any for e) if self.mouse.drag(&[self.tab_title_area], e) => {
496 if let Some(n) = self.mouse.item_at(&self.tab_title_areas, e.column, e.row) {
497 self.select(Some(n));
498 TabbedOutcome::Select(n)
499 } else {
500 TabbedOutcome::Unchanged
501 }
502 }
503 ct_event!(mouse down Left for x, y)
504 if self.tab_title_area.contains((*x, *y).into()) =>
505 {
506 if let Some(sel) = self.mouse.item_at(&self.tab_title_close_areas, *x, *y) {
507 TabbedOutcome::Close(sel)
508 } else if let Some(sel) = self.mouse.item_at(&self.tab_title_areas, *x, *y) {
509 self.select(Some(sel));
510 TabbedOutcome::Select(sel)
511 } else {
512 TabbedOutcome::Continue
513 }
514 }
515
516 _ => TabbedOutcome::Continue,
517 }
518 }
519}
520
521trait TabWidget: Debug {
526 fn layout(
528 &self, area: Rect,
530 tabbed: &Tabbed<'_>,
531 state: &mut TabbedState,
532 );
533
534 fn render(
536 &self, buf: &mut Buffer,
538 tabbed: &Tabbed<'_>,
539 state: &mut TabbedState,
540 );
541}