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