1use crate::_private::NonExhaustive;
5use crate::event::TabbedOutcome;
6use crate::tabbed::attached::AttachedTabs;
7use crate::tabbed::glued::GluedTabs;
8use rat_event::util::MouseFlagsN;
9use rat_event::{ct_event, flow, HandleEvent, MouseOnly, Regular};
10use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
11use rat_reloc::{relocate_area, relocate_areas, RelocatableState};
12use ratatui::buffer::Buffer;
13use ratatui::layout::Rect;
14use ratatui::style::Style;
15use ratatui::text::Line;
16#[cfg(feature = "unstable-widget-ref")]
17use ratatui::widgets::StatefulWidgetRef;
18use ratatui::widgets::{Block, StatefulWidget};
19use std::cmp::min;
20use std::fmt::Debug;
21
22mod attached;
23mod glued;
24
25#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
30pub enum TabPlacement {
31 #[default]
34 Top,
35 Left,
38 Right,
41 Bottom,
44}
45
46#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum TabType {
50 Glued,
52
53 #[default]
61 Attached,
62}
63
64#[derive(Debug, Default)]
72pub struct Tabbed<'a> {
73 tab_type: TabType,
74 placement: TabPlacement,
75 closeable: bool,
76 tabs: Vec<Line<'a>>,
77 block: Option<Block<'a>>,
78
79 style: Style,
80 tab_style: Option<Style>,
81 select_style: Option<Style>,
82 focus_style: Option<Style>,
83}
84
85#[derive(Debug, Clone)]
87pub struct TabbedStyle {
88 pub style: Style,
89 pub tab: Option<Style>,
90 pub select: Option<Style>,
91 pub focus: Option<Style>,
92
93 pub tab_type: Option<TabType>,
94 pub placement: Option<TabPlacement>,
95 pub block: Option<Block<'static>>,
96
97 pub non_exhaustive: NonExhaustive,
98}
99
100#[derive(Debug, Default)]
102pub struct TabbedState {
103 pub area: Rect,
106 pub block_area: Rect,
109 pub widget_area: Rect,
113
114 pub tab_title_area: Rect,
117 pub tab_title_areas: Vec<Rect>,
120 pub tab_title_close_areas: Vec<Rect>,
123
124 pub selected: Option<usize>,
128
129 pub focus: FocusFlag,
132 pub mouse: MouseFlagsN,
135}
136
137pub(crate) mod event {
138 use rat_event::{ConsumedEvent, Outcome};
139
140 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
142 pub enum TabbedOutcome {
143 Continue,
145 Unchanged,
148 Changed,
153 Select(usize),
155 Close(usize),
157 }
158
159 impl ConsumedEvent for TabbedOutcome {
160 fn is_consumed(&self) -> bool {
161 *self != TabbedOutcome::Continue
162 }
163 }
164
165 impl From<bool> for TabbedOutcome {
167 fn from(value: bool) -> Self {
168 if value {
169 TabbedOutcome::Changed
170 } else {
171 TabbedOutcome::Unchanged
172 }
173 }
174 }
175
176 impl From<Outcome> for TabbedOutcome {
177 fn from(value: Outcome) -> Self {
178 match value {
179 Outcome::Continue => TabbedOutcome::Continue,
180 Outcome::Unchanged => TabbedOutcome::Unchanged,
181 Outcome::Changed => TabbedOutcome::Changed,
182 }
183 }
184 }
185
186 impl From<TabbedOutcome> for Outcome {
187 fn from(value: TabbedOutcome) -> Self {
188 match value {
189 TabbedOutcome::Continue => Outcome::Continue,
190 TabbedOutcome::Unchanged => Outcome::Unchanged,
191 TabbedOutcome::Changed => Outcome::Changed,
192 TabbedOutcome::Select(_) => Outcome::Changed,
193 TabbedOutcome::Close(_) => Outcome::Changed,
194 }
195 }
196 }
197}
198
199impl<'a> Tabbed<'a> {
200 pub fn new() -> Self {
201 Self::default()
202 }
203
204 pub fn tab_type(mut self, tab_type: TabType) -> Self {
206 self.tab_type = tab_type;
207 self
208 }
209
210 pub fn placement(mut self, placement: TabPlacement) -> Self {
212 self.placement = placement;
213 self
214 }
215
216 pub fn tabs(mut self, tabs: impl IntoIterator<Item = impl Into<Line<'a>>>) -> Self {
218 self.tabs = tabs.into_iter().map(|v| v.into()).collect::<Vec<_>>();
219 self
220 }
221
222 pub fn closeable(mut self, closeable: bool) -> Self {
226 self.closeable = closeable;
227 self
228 }
229
230 pub fn block(mut self, block: Block<'a>) -> Self {
232 self.block = Some(block);
233 self
234 }
235
236 pub fn styles(mut self, styles: TabbedStyle) -> Self {
238 self.style = styles.style;
239 if styles.tab.is_some() {
240 self.tab_style = styles.tab;
241 }
242 if styles.select.is_some() {
243 self.select_style = styles.select;
244 }
245 if styles.focus.is_some() {
246 self.focus_style = styles.focus;
247 }
248 if let Some(tab_type) = styles.tab_type {
249 self.tab_type = tab_type;
250 }
251 if let Some(placement) = styles.placement {
252 self.placement = placement
253 }
254 if styles.block.is_some() {
255 self.block = styles.block;
256 }
257 self
258 }
259
260 pub fn style(mut self, style: Style) -> Self {
262 self.style = style;
263 self
264 }
265
266 pub fn tab_style(mut self, style: Style) -> Self {
268 self.tab_style = Some(style);
269 self
270 }
271
272 pub fn select_style(mut self, style: Style) -> Self {
274 self.select_style = Some(style);
275 self
276 }
277
278 pub fn focus_style(mut self, style: Style) -> Self {
280 self.focus_style = Some(style);
281 self
282 }
283}
284
285impl Default for TabbedStyle {
286 fn default() -> Self {
287 Self {
288 style: Default::default(),
289 tab: None,
290 select: None,
291 focus: None,
292 tab_type: None,
293 placement: None,
294 block: None,
295 non_exhaustive: NonExhaustive,
296 }
297 }
298}
299
300impl StatefulWidget for Tabbed<'_> {
301 type State = TabbedState;
302
303 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
304 render_ref(&self, area, buf, state);
305 }
306}
307
308#[cfg(feature = "unstable-widget-ref")]
309impl<'a> StatefulWidgetRef for Tabbed<'a> {
310 type State = TabbedState;
311
312 fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
313 render_ref(self, area, buf, state);
314 }
315}
316
317fn render_ref(tabbed: &Tabbed<'_>, area: Rect, buf: &mut Buffer, state: &mut TabbedState) {
318 if tabbed.tabs.is_empty() {
319 state.selected = None;
320 } else {
321 if state.selected.is_none() {
322 state.selected = Some(0);
323 }
324 }
325
326 match tabbed.tab_type {
327 TabType::Glued => {
328 GluedTabs.layout(area, tabbed, state);
329 GluedTabs.render(buf, tabbed, state);
330 }
331 TabType::Attached => {
332 AttachedTabs.layout(area, tabbed, state);
333 AttachedTabs.render(buf, tabbed, state);
334 }
335 }
336}
337
338impl Clone for TabbedState {
339 fn clone(&self) -> Self {
340 Self {
341 area: self.area,
342 block_area: self.block_area,
343 widget_area: self.widget_area,
344 tab_title_area: self.tab_title_area,
345 tab_title_areas: self.tab_title_areas.clone(),
346 tab_title_close_areas: self.tab_title_close_areas.clone(),
347 selected: self.selected,
348 focus: FocusFlag::named(self.focus.name()),
349 mouse: Default::default(),
350 }
351 }
352}
353
354impl HasFocus for TabbedState {
355 fn build(&self, builder: &mut FocusBuilder) {
356 builder.leaf_widget(self);
357 }
358
359 fn focus(&self) -> FocusFlag {
360 self.focus.clone()
361 }
362
363 fn area(&self) -> Rect {
364 Rect::default()
365 }
366
367 fn navigable(&self) -> Navigation {
368 Navigation::Leave
369 }
370}
371
372impl RelocatableState for TabbedState {
373 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
374 self.area = relocate_area(self.area, shift, clip);
375 self.block_area = relocate_area(self.block_area, shift, clip);
376 self.widget_area = relocate_area(self.widget_area, shift, clip);
377 self.tab_title_area = relocate_area(self.tab_title_area, shift, clip);
378 relocate_areas(self.tab_title_areas.as_mut(), shift, clip);
379 }
380}
381
382impl TabbedState {
383 pub fn new() -> Self {
385 Default::default()
386 }
387
388 pub fn named(name: &str) -> Self {
390 Self {
391 focus: FocusFlag::named(name),
392 ..Default::default()
393 }
394 }
395
396 pub fn selected(&self) -> Option<usize> {
397 self.selected
398 }
399
400 pub fn select(&mut self, selected: Option<usize>) {
401 self.selected = selected;
402 }
403
404 pub fn next_tab(&mut self) -> bool {
406 let old_selected = self.selected;
407
408 if let Some(selected) = self.selected() {
409 self.selected = Some(min(
410 selected + 1,
411 self.tab_title_areas.len().saturating_sub(1),
412 ));
413 }
414
415 old_selected != self.selected
416 }
417
418 pub fn prev_tab(&mut self) -> bool {
420 let old_selected = self.selected;
421
422 if let Some(selected) = self.selected() {
423 if selected > 0 {
424 self.selected = Some(selected - 1);
425 }
426 }
427
428 old_selected != self.selected
429 }
430}
431
432impl HandleEvent<crossterm::event::Event, Regular, TabbedOutcome> for TabbedState {
434 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> TabbedOutcome {
435 if self.is_focused() {
436 flow!(match event {
437 ct_event!(keycode press Right) => self.next_tab().into(),
438 ct_event!(keycode press Left) => self.prev_tab().into(),
439 _ => TabbedOutcome::Continue,
440 });
441 }
442
443 self.handle(event, MouseOnly)
444 }
445}
446
447impl HandleEvent<crossterm::event::Event, MouseOnly, TabbedOutcome> for TabbedState {
448 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> TabbedOutcome {
449 match event {
450 ct_event!(mouse any for e) if self.mouse.hover(&self.tab_title_close_areas, e) => {
451 TabbedOutcome::Changed
452 }
453 ct_event!(mouse any for e) if self.mouse.drag(&[self.tab_title_area], e) => {
454 if let Some(n) = self.mouse.item_at(&self.tab_title_areas, e.column, e.row) {
455 self.select(Some(n));
456 TabbedOutcome::Select(n)
457 } else {
458 TabbedOutcome::Unchanged
459 }
460 }
461 ct_event!(mouse down Left for x, y)
462 if self.tab_title_area.contains((*x, *y).into()) =>
463 {
464 if let Some(sel) = self.mouse.item_at(&self.tab_title_close_areas, *x, *y) {
465 TabbedOutcome::Close(sel)
466 } else if let Some(sel) = self.mouse.item_at(&self.tab_title_areas, *x, *y) {
467 self.select(Some(sel));
468 TabbedOutcome::Select(sel)
469 } else {
470 TabbedOutcome::Continue
471 }
472 }
473
474 _ => TabbedOutcome::Continue,
475 }
476 }
477}
478
479trait TabWidget: Debug {
484 fn layout(
486 &self, area: Rect,
488 tabbed: &Tabbed<'_>,
489 state: &mut TabbedState,
490 );
491
492 fn render(
494 &self, buf: &mut Buffer,
496 tabbed: &Tabbed<'_>,
497 state: &mut TabbedState,
498 );
499}