mirror of
https://codeberg.org/JasterV/teatui.git
synced 2026-04-26 18:10:03 +00:00
feat: add new example for working with lists
This commit is contained in:
parent
92118d929e
commit
4d7ba57f96
7 changed files with 635 additions and 0 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -1304,6 +1304,16 @@ version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "todo_list"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"crossterm",
|
||||||
|
"ratatui",
|
||||||
|
"strum",
|
||||||
|
"teatui",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.49.0"
|
version = "1.49.0"
|
||||||
|
|
|
||||||
12
examples/todo-list/Cargo.toml
Normal file
12
examples/todo-list/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "todo_list"
|
||||||
|
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" }
|
||||||
18
examples/todo-list/src/main.rs
Normal file
18
examples/todo-list/src/main.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
//! # [TeaTui] List example
|
||||||
|
|
||||||
|
use crate::model::Model;
|
||||||
|
use message::Message;
|
||||||
|
use teatui::ProgramError;
|
||||||
|
|
||||||
|
mod message;
|
||||||
|
mod model;
|
||||||
|
mod update;
|
||||||
|
mod view;
|
||||||
|
|
||||||
|
fn main() -> Result<(), ProgramError<Model, Message, ()>> {
|
||||||
|
teatui::start(init, update::update, view::view, |_, _| None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init() -> (Model, Option<()>) {
|
||||||
|
(Model::default(), None)
|
||||||
|
}
|
||||||
75
examples/todo-list/src/message.rs
Normal file
75
examples/todo-list/src/message.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Message {
|
||||||
|
NoOp,
|
||||||
|
Exit,
|
||||||
|
SelectNext,
|
||||||
|
SelectNone,
|
||||||
|
SelectPrevious,
|
||||||
|
SelectFirst,
|
||||||
|
SelectLast,
|
||||||
|
ToggleStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crossterm::event::Event> for Message {
|
||||||
|
fn from(value: crossterm::event::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::Char(' ') | KeyCode::Right,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: _,
|
||||||
|
modifiers: _,
|
||||||
|
}) => Self::ToggleStatus,
|
||||||
|
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('h') | KeyCode::Left,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: _,
|
||||||
|
modifiers: _,
|
||||||
|
}) => Self::SelectNone,
|
||||||
|
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('j') | KeyCode::Down,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: _,
|
||||||
|
modifiers: _,
|
||||||
|
}) => Self::SelectNext,
|
||||||
|
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('k') | KeyCode::Up,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: _,
|
||||||
|
modifiers: _,
|
||||||
|
}) => Self::SelectPrevious,
|
||||||
|
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('g') | KeyCode::Home,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: _,
|
||||||
|
modifiers: _,
|
||||||
|
}) => Self::SelectFirst,
|
||||||
|
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('G') | KeyCode::End,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: _,
|
||||||
|
modifiers: _,
|
||||||
|
}) => Self::SelectLast,
|
||||||
|
|
||||||
|
Event::FocusGained
|
||||||
|
| Event::FocusLost
|
||||||
|
| Event::Key(_)
|
||||||
|
| Event::Mouse(_)
|
||||||
|
| Event::Paste(_)
|
||||||
|
| Event::Resize(_, _) => Self::NoOp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
364
examples/todo-list/src/model.rs
Normal file
364
examples/todo-list/src/model.rs
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
use ratatui::widgets::ListState;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Model {
|
||||||
|
pub items: Vec<TodoItem>,
|
||||||
|
pub state: ListState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TodoItem {
|
||||||
|
pub todo: String,
|
||||||
|
pub info: String,
|
||||||
|
pub status: Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum Status {
|
||||||
|
Todo,
|
||||||
|
Completed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Model {
|
||||||
|
fn default() -> Self {
|
||||||
|
Model::from_iter([
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Rewrite everything with Rust!",
|
||||||
|
"I can't hold my inner voice. He tells me to rewrite the complete universe with Rust",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Rewrite all of your tui apps with Ratatui",
|
||||||
|
"Yes, you heard that right. Go and replace your tui with Ratatui.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Pet your cat",
|
||||||
|
"Minnak loves to be pet by you! Don't forget to pet and give some treats!",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Walk with your dog",
|
||||||
|
"Max is bored, go walk with him!",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Pay the bills",
|
||||||
|
"Pay the train subscription!!!",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Refactor list example",
|
||||||
|
"If you see this info that means I completed this task!",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Implement a neural network in pure Rust",
|
||||||
|
"Because why use Python when you can have zero-cost abstractions and borrow checker headaches?",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Buy more coffee",
|
||||||
|
"The compile times are getting longer, or maybe I'm just getting slower. Either way, caffeine is required.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Fix that one annoying lifetime error",
|
||||||
|
"It only took three days and a sacrifice to the crab god, but it finally compiles!",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Clean the mechanical keyboard",
|
||||||
|
"There is a non-zero amount of crumbs between the blue switches. It’s affecting my WPM.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Read the 'Rustonomicon'",
|
||||||
|
"Into the dark depths of unsafe Rust we go. May the pointer aliasing rules have mercy.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Hydrate",
|
||||||
|
"Drank a full glass of water. Look at me, practicing self-care while writing low-level code.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Argue about memory safety on the internet",
|
||||||
|
"Someone said C is 'fine.' I must cordially explain why they are mistaken.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Organize the .dotfiles",
|
||||||
|
"Spend 4 hours configuring Neovim instead of actually working on the project.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Update dependencies",
|
||||||
|
"Ran 'cargo update'. Only 14 breaking changes in the ecosystem today. A new record!",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Research async-trait internals",
|
||||||
|
"Why is it a macro? Why is life a macro? I need to know.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Go to the gym",
|
||||||
|
"Strong body, strong memory management. Don't let your muscles leak like a C++ string.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Explain ownership to a rubber duck",
|
||||||
|
"The duck didn't get it at first, but after a few squeaks, it finally understood move semantics.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Check for compiler updates",
|
||||||
|
"Is there a new nightly? I need those experimental features for no reason at all.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Debug the deadlocked thread",
|
||||||
|
"It's not a bug, it's just a very dedicated pause in execution.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Star every crate I use on GitHub",
|
||||||
|
"Showing love to the maintainers who keep my 'cargo build' alive.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Write a custom proc-macro",
|
||||||
|
"I want to generate code that generates code. We need to go deeper.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Finally learn how Pin works",
|
||||||
|
"I've read the docs five times. Maybe the sixth time is the charm?",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Delete the 'node_modules' folder",
|
||||||
|
"Regained 40GB of disk space. Nature is healing.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Optimize the Docker image size",
|
||||||
|
"From 1.2GB to 10MB using multi-stage builds and Alpine. Efficiency feels good.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Fix the CI/CD pipeline",
|
||||||
|
"It works on my machine, but GitHub Actions thinks otherwise.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Replace a 5-line bash script with 200 lines of Rust",
|
||||||
|
"Was it worth it? Yes. It's type-safe now.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Actually write documentation",
|
||||||
|
"The code is the documentation... said no one ever (and lived).",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Try a different terminal emulator",
|
||||||
|
"Is Alacritty faster than Kitty? Let the benchmarking begin.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Nap for 20 minutes",
|
||||||
|
"Brain was stuck in a recursion loop. Had to reboot the system.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Configure a status bar for the TWM",
|
||||||
|
"I need to see my CPU temperature in 16-bit color at all times.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Rewrite the parser using Nom",
|
||||||
|
"Regex is great, but combinators are cooler.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Find a cool Ferris sticker",
|
||||||
|
"My laptop lid still has 2 square inches of empty space.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Master Vim motions",
|
||||||
|
"H, J, K, L... why did I just delete the entire main function?",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Experiment with WebAssembly",
|
||||||
|
"Rust in the browser is the future, and the future is now.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Add a dark mode to the UI",
|
||||||
|
"My eyes are no longer burning. Success.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Submit a PR to an open-source project",
|
||||||
|
"Fixing a typo counts as a contribution, right?",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Explain Cow to a non-programmer",
|
||||||
|
"No, not the animal. It's 'Clone On Write'. Why are you walking away?",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Organize the physical desk",
|
||||||
|
"Found a missing USB drive and three half-empty coffee mugs.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Write unit tests for the edge cases",
|
||||||
|
"What happens if the input is an emoji and a null byte? Let's find out.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Learn a new functional programming language",
|
||||||
|
"Just to see how it feels. Don't worry Rust, I'm not leaving you.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Solve a LeetCode hard in Rust",
|
||||||
|
"The borrow checker was the real final boss.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Set up a Raspberry Pi server",
|
||||||
|
"It will sit in the corner and run a Telegram bot for two days before I forget about it.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Refactor the error handling",
|
||||||
|
"Switching from 'unwrap()' to 'thiserror' because I'm a professional now.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Explain the orphan rule to a coworker",
|
||||||
|
"I think I confused them more, but I feel smarter.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Benchmark the hot path",
|
||||||
|
"Flamegraphs are like modern art, but for nerds.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Todo,
|
||||||
|
"Buy a more ergonomic chair",
|
||||||
|
"My back is screaming in a language I don't understand.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Status::Completed,
|
||||||
|
"Finally finish this list",
|
||||||
|
"36 items in total. Now I just have to actually do them.",
|
||||||
|
),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromIterator<(Status, &'static str, &'static str)> for Model {
|
||||||
|
fn from_iter<I: IntoIterator<Item = (Status, &'static str, &'static str)>>(iter: I) -> Self {
|
||||||
|
let items: Vec<_> = iter
|
||||||
|
.into_iter()
|
||||||
|
.map(|(status, todo, info)| TodoItem::new(status, todo, info))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let state = ListState::default().with_selected(Some(0));
|
||||||
|
|
||||||
|
Self { items, state }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TodoItem {
|
||||||
|
fn new(status: Status, todo: &str, info: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
status,
|
||||||
|
todo: todo.to_string(),
|
||||||
|
info: info.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub fn selected(&self) -> Option<&TodoItem> {
|
||||||
|
self.state
|
||||||
|
.selected()
|
||||||
|
.and_then(|index| self.items.get(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_none(self) -> Self {
|
||||||
|
let state = ListState::default().with_selected(None);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
items: self.items,
|
||||||
|
state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_first(self) -> Self {
|
||||||
|
let state = ListState::default().with_selected(Some(0));
|
||||||
|
|
||||||
|
Model {
|
||||||
|
items: self.items,
|
||||||
|
state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_last(self) -> Self {
|
||||||
|
let state = ListState::default().with_selected(Some(self.items.len() - 1));
|
||||||
|
|
||||||
|
Model {
|
||||||
|
items: self.items,
|
||||||
|
state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_next(self) -> Self {
|
||||||
|
match self.state.selected() {
|
||||||
|
Some(index) => {
|
||||||
|
let state =
|
||||||
|
ListState::default().with_selected(Some((index + 1) % self.items.len()));
|
||||||
|
|
||||||
|
Model { state, ..self }
|
||||||
|
}
|
||||||
|
None => self.select_first(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_previous(self) -> Self {
|
||||||
|
match self.state.selected() {
|
||||||
|
Some(0) => self.select_last(),
|
||||||
|
Some(index) => {
|
||||||
|
let state = ListState::default().with_selected(Some(index.saturating_sub(1)));
|
||||||
|
Model { state, ..self }
|
||||||
|
}
|
||||||
|
None => self.select_last(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_status(mut self) -> Self {
|
||||||
|
let selected_index = self.state.selected();
|
||||||
|
|
||||||
|
match selected_index {
|
||||||
|
Some(index) => {
|
||||||
|
self.items[index].status = match self.items[index].status {
|
||||||
|
Status::Todo => Status::Completed,
|
||||||
|
Status::Completed => Status::Todo,
|
||||||
|
};
|
||||||
|
|
||||||
|
Model { ..self }
|
||||||
|
}
|
||||||
|
None => Model { ..self },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
examples/todo-list/src/update.rs
Normal file
16
examples/todo-list/src/update.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
use teatui::update::Update;
|
||||||
|
|
||||||
|
use crate::{message::Message, model::Model};
|
||||||
|
|
||||||
|
pub fn update(model: Model, msg: Message) -> Update<Model, ()> {
|
||||||
|
match msg {
|
||||||
|
Message::NoOp => Update::Next(model, None),
|
||||||
|
Message::Exit => Update::Exit,
|
||||||
|
Message::SelectNext => Update::Next(model.select_next(), None),
|
||||||
|
Message::SelectNone => Update::Next(model.select_none(), None),
|
||||||
|
Message::SelectPrevious => Update::Next(model.select_previous(), None),
|
||||||
|
Message::SelectFirst => Update::Next(model.select_first(), None),
|
||||||
|
Message::SelectLast => Update::Next(model.select_last(), None),
|
||||||
|
Message::ToggleStatus => Update::Next(model.toggle_status(), None),
|
||||||
|
}
|
||||||
|
}
|
||||||
140
examples/todo-list/src/view.rs
Normal file
140
examples/todo-list/src/view.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Layout, Rect},
|
||||||
|
style::{
|
||||||
|
Color, Modifier, Style, Stylize,
|
||||||
|
palette::tailwind::{BLUE, GREEN, SLATE},
|
||||||
|
},
|
||||||
|
symbols,
|
||||||
|
text::Line,
|
||||||
|
widgets::{
|
||||||
|
Block, Borders, HighlightSpacing, List, ListItem, Padding, Paragraph, StatefulWidget,
|
||||||
|
Widget, Wrap,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::model::{Model, Status, TodoItem};
|
||||||
|
|
||||||
|
const TODO_HEADER_STYLE: Style = Style::new().fg(SLATE.c100).bg(BLUE.c800);
|
||||||
|
const NORMAL_ROW_BG: Color = SLATE.c950;
|
||||||
|
const ALT_ROW_BG_COLOR: Color = SLATE.c900;
|
||||||
|
const SELECTED_STYLE: Style = Style::new().bg(SLATE.c800).add_modifier(Modifier::BOLD);
|
||||||
|
const TEXT_FG_COLOR: Color = SLATE.c200;
|
||||||
|
const COMPLETED_TEXT_FG_COLOR: Color = GREEN.c500;
|
||||||
|
|
||||||
|
pub fn view(model: Model) -> AppWidget {
|
||||||
|
AppWidget { model }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppWidget {
|
||||||
|
model: Model,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for AppWidget {
|
||||||
|
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let [header_area, main_area, footer_area] = Layout::vertical([
|
||||||
|
Constraint::Length(2),
|
||||||
|
Constraint::Fill(1),
|
||||||
|
Constraint::Length(1),
|
||||||
|
])
|
||||||
|
.areas(area);
|
||||||
|
|
||||||
|
let [list_area, item_area] =
|
||||||
|
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(main_area);
|
||||||
|
|
||||||
|
Paragraph::new("Ratatui List Example")
|
||||||
|
.bold()
|
||||||
|
.centered()
|
||||||
|
.render(header_area, buf);
|
||||||
|
|
||||||
|
Paragraph::new("Use ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.")
|
||||||
|
.centered()
|
||||||
|
.render(footer_area, buf);
|
||||||
|
|
||||||
|
self.render_list(list_area, buf);
|
||||||
|
self.render_selected_item(item_area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rendering logic for the app
|
||||||
|
impl AppWidget {
|
||||||
|
fn render_list(&mut self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let block = Block::new()
|
||||||
|
.title(Line::raw("TODO List").centered())
|
||||||
|
.borders(Borders::TOP)
|
||||||
|
.border_set(symbols::border::EMPTY)
|
||||||
|
.border_style(TODO_HEADER_STYLE)
|
||||||
|
.bg(NORMAL_ROW_BG);
|
||||||
|
|
||||||
|
// Iterate through all elements in the `items` and stylize them.
|
||||||
|
let items: Vec<ListItem> = self
|
||||||
|
.model
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, todo_item)| {
|
||||||
|
let color = alternate_colors(i);
|
||||||
|
ListItem::from(todo_item).bg(color)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Create a List from all list items and highlight the currently selected one
|
||||||
|
let list = List::new(items)
|
||||||
|
.block(block)
|
||||||
|
.highlight_style(SELECTED_STYLE)
|
||||||
|
.highlight_symbol(">")
|
||||||
|
.highlight_spacing(HighlightSpacing::Always);
|
||||||
|
|
||||||
|
// We need to disambiguate this trait method as both `Widget` and `StatefulWidget` share the
|
||||||
|
// same method name `render`.
|
||||||
|
StatefulWidget::render(list, area, buf, &mut self.model.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_selected_item(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
// We get the info depending on the item's state.
|
||||||
|
let info = if let Some(item) = self.model.selected() {
|
||||||
|
match item.status {
|
||||||
|
Status::Completed => format!("✓ DONE: {}", item.info),
|
||||||
|
Status::Todo => format!("☐ TODO: {}", item.info),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"Nothing selected...".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// We show the list item's info under the list in this paragraph
|
||||||
|
let block = Block::new()
|
||||||
|
.title(Line::raw("TODO Info").centered())
|
||||||
|
.borders(Borders::TOP)
|
||||||
|
.border_set(symbols::border::EMPTY)
|
||||||
|
.border_style(TODO_HEADER_STYLE)
|
||||||
|
.bg(NORMAL_ROW_BG)
|
||||||
|
.padding(Padding::horizontal(1));
|
||||||
|
|
||||||
|
// We can now render the item info
|
||||||
|
Paragraph::new(info)
|
||||||
|
.block(block)
|
||||||
|
.fg(TEXT_FG_COLOR)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn alternate_colors(i: usize) -> Color {
|
||||||
|
if i % 2 == 0 {
|
||||||
|
NORMAL_ROW_BG
|
||||||
|
} else {
|
||||||
|
ALT_ROW_BG_COLOR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&TodoItem> for ListItem<'_> {
|
||||||
|
fn from(value: &TodoItem) -> Self {
|
||||||
|
let line = match value.status {
|
||||||
|
Status::Todo => Line::styled(format!(" ☐ {}", value.todo), TEXT_FG_COLOR),
|
||||||
|
Status::Completed => {
|
||||||
|
Line::styled(format!(" ✓ {}", value.todo), COMPLETED_TEXT_FG_COLOR)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ListItem::new(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue