mirror of
https://codeberg.org/JasterV/teatui.git
synced 2026-04-27 02:15:41 +00:00
feat: add a new example for working with tabs
This commit is contained in:
parent
c24d7762cb
commit
92118d929e
3 changed files with 255 additions and 1 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -1160,9 +1160,19 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tabs"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"crossterm",
|
||||||
|
"ratatui",
|
||||||
|
"strum",
|
||||||
|
"teatui",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "teatui"
|
name = "teatui"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
|
|
||||||
12
examples/tabs/Cargo.toml
Normal file
12
examples/tabs/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "tabs"
|
||||||
|
publish = false
|
||||||
|
version = "0.0.0"
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
crossterm.workspace = true
|
||||||
|
ratatui.workspace = true
|
||||||
|
strum = "0.27.2"
|
||||||
|
teatui = { path = "../../teatui" }
|
||||||
232
examples/tabs/src/main.rs
Normal file
232
examples/tabs/src/main.rs
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Layout, Rect},
|
||||||
|
style::{Color, Stylize, palette::tailwind},
|
||||||
|
symbols,
|
||||||
|
text::Line,
|
||||||
|
widgets::{Block, Padding, Paragraph, Tabs, Widget},
|
||||||
|
};
|
||||||
|
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||||
|
use teatui::{ProgramError, update::Update};
|
||||||
|
|
||||||
|
fn main() -> Result<(), ProgramError<Model, Message, ()>> {
|
||||||
|
teatui::start(init, update, view, |_, _| None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init() -> (Model, Option<()>) {
|
||||||
|
(Model::default(), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Defines the state of the application
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct Model {
|
||||||
|
selected_tab: SelectedTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub fn next_tab(self) -> Self {
|
||||||
|
Model {
|
||||||
|
selected_tab: self.selected_tab.next(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous_tab(self) -> Self {
|
||||||
|
Model {
|
||||||
|
selected_tab: self.selected_tab.previous(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, Display, FromRepr, EnumIter)]
|
||||||
|
enum SelectedTab {
|
||||||
|
#[default]
|
||||||
|
#[strum(to_string = "Tab 1")]
|
||||||
|
Tab1,
|
||||||
|
#[strum(to_string = "Tab 2")]
|
||||||
|
Tab2,
|
||||||
|
#[strum(to_string = "Tab 3")]
|
||||||
|
Tab3,
|
||||||
|
#[strum(to_string = "Tab 4")]
|
||||||
|
Tab4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectedTab {
|
||||||
|
/// Get the previous tab, if there is no previous tab return the current tab.
|
||||||
|
fn previous(self) -> Self {
|
||||||
|
let current_index: usize = self as usize;
|
||||||
|
let previous_index = current_index.saturating_sub(1);
|
||||||
|
Self::from_repr(previous_index).unwrap_or(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the next tab, if there is no next tab return the current tab.
|
||||||
|
fn next(self) -> Self {
|
||||||
|
let current_index = self as usize;
|
||||||
|
let next_index = current_index.saturating_add(1);
|
||||||
|
Self::from_repr(next_index).unwrap_or(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Messages that represent a change of state in the application
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Message {
|
||||||
|
NextTab,
|
||||||
|
PreviousTab,
|
||||||
|
Exit,
|
||||||
|
NoOp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crossterm::event::Event> for Message {
|
||||||
|
fn from(value: Event) -> Self {
|
||||||
|
match value {
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Esc | KeyCode::Char('q'),
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: _,
|
||||||
|
modifiers: _,
|
||||||
|
}) => Self::Exit,
|
||||||
|
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('l') | KeyCode::Right,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: _,
|
||||||
|
modifiers: _,
|
||||||
|
}) => Self::NextTab,
|
||||||
|
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('h') | KeyCode::Left,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: _,
|
||||||
|
modifiers: _,
|
||||||
|
}) => Self::PreviousTab,
|
||||||
|
|
||||||
|
Event::FocusGained
|
||||||
|
| Event::FocusLost
|
||||||
|
| Event::Key(_)
|
||||||
|
| Event::Mouse(_)
|
||||||
|
| Event::Paste(_)
|
||||||
|
| Event::Resize(_, _) => Self::NoOp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elm-like update function.
|
||||||
|
///
|
||||||
|
/// Given the current state (model) and an incoming message from the outside world,
|
||||||
|
/// return the next updated state
|
||||||
|
fn update(model: Model, msg: Message) -> Update<Model, ()> {
|
||||||
|
match msg {
|
||||||
|
Message::Exit => Update::Exit,
|
||||||
|
Message::NoOp => Update::Next(model, None),
|
||||||
|
Message::NextTab => Update::Next(Model::next_tab(model), None),
|
||||||
|
Message::PreviousTab => Update::Next(Model::previous_tab(model), None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elm-like View function.
|
||||||
|
///
|
||||||
|
/// Given the current state, return a drawable widget.
|
||||||
|
fn view(model: Model) -> AppWidget {
|
||||||
|
AppWidget { model }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppWidget {
|
||||||
|
model: Model,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for AppWidget {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
use Constraint::{Length, Min};
|
||||||
|
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
|
||||||
|
let [header_area, inner_area, footer_area] = vertical.areas(area);
|
||||||
|
|
||||||
|
let horizontal = Layout::horizontal([Min(0), Length(20)]);
|
||||||
|
let [tabs_area, title_area] = horizontal.areas(header_area);
|
||||||
|
|
||||||
|
"Ratatui Tabs Example".bold().render(title_area, buf);
|
||||||
|
|
||||||
|
render_tabs(&self.model.selected_tab, tabs_area, buf);
|
||||||
|
|
||||||
|
self.model.selected_tab.render(inner_area, buf);
|
||||||
|
|
||||||
|
Line::raw("◄ ► to change tab | Press q to quit")
|
||||||
|
.centered()
|
||||||
|
.render(footer_area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_tabs(selected_tab: &SelectedTab, area: Rect, buf: &mut Buffer) {
|
||||||
|
let titles = SelectedTab::iter().map(SelectedTab::title);
|
||||||
|
let highlight_style = (Color::default(), selected_tab.palette().c700);
|
||||||
|
let selected_tab_index = (*selected_tab) as usize;
|
||||||
|
|
||||||
|
Tabs::new(titles)
|
||||||
|
.highlight_style(highlight_style)
|
||||||
|
.select(selected_tab_index)
|
||||||
|
.padding("", "")
|
||||||
|
.divider(" ")
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for SelectedTab {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
// in a real app these might be separate widgets
|
||||||
|
match self {
|
||||||
|
Self::Tab1 => self.render_tab0(area, buf),
|
||||||
|
Self::Tab2 => self.render_tab1(area, buf),
|
||||||
|
Self::Tab3 => self.render_tab2(area, buf),
|
||||||
|
Self::Tab4 => self.render_tab3(area, buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectedTab {
|
||||||
|
/// Return tab's name as a styled `Line`
|
||||||
|
fn title(self) -> Line<'static> {
|
||||||
|
format!(" {self} ")
|
||||||
|
.fg(tailwind::SLATE.c200)
|
||||||
|
.bg(self.palette().c900)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_tab0(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
Paragraph::new("Hello, World!")
|
||||||
|
.block(self.block())
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_tab1(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
Paragraph::new("Welcome to the Ratatui tabs example!")
|
||||||
|
.block(self.block())
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_tab2(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
Paragraph::new("Look! I'm different than others!")
|
||||||
|
.block(self.block())
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_tab3(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
Paragraph::new("I know, these are some basic changes. But I think you got the main idea.")
|
||||||
|
.block(self.block())
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A block surrounding the tab's content
|
||||||
|
fn block(self) -> Block<'static> {
|
||||||
|
Block::bordered()
|
||||||
|
.border_set(symbols::border::PROPORTIONAL_TALL)
|
||||||
|
.padding(Padding::horizontal(1))
|
||||||
|
.border_style(self.palette().c700)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn palette(self) -> tailwind::Palette {
|
||||||
|
match self {
|
||||||
|
Self::Tab1 => tailwind::BLUE,
|
||||||
|
Self::Tab2 => tailwind::EMERALD,
|
||||||
|
Self::Tab3 => tailwind::INDIGO,
|
||||||
|
Self::Tab4 => tailwind::RED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue