Compare commits
10 Commits
cc7bb4a15f
...
511046678d
| Author | SHA1 | Date | |
|---|---|---|---|
| 511046678d | |||
| 7e896d04bd | |||
| 79842ae70d | |||
| f508cd9194 | |||
| 00f231d79f | |||
| 33cd32ce40 | |||
| b0726b1fb4 | |||
| 2644f68cab | |||
| 0c765a608b | |||
| 97457af448 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -454,7 +454,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lool"
|
name = "lool"
|
||||||
version = "0.6.1"
|
version = "0.9.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
22
Cargo.toml
22
Cargo.toml
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lool"
|
name = "lool"
|
||||||
version = "0.6.1"
|
version = "0.9.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "🧳 lool » lucode.ar rust common utilities"
|
description = "🧳 lool » lucode.ar rust common utilities"
|
||||||
authors = ["Lucas Colombo <lucas@lucode.ar>"]
|
authors = ["Lucas Colombo <lucas@lucode.ar>"]
|
||||||
@ -64,6 +64,11 @@ path = "lib/lib.rs"
|
|||||||
"utils" = []
|
"utils" = []
|
||||||
"utils.threads" = ["utils", "macros", "dep:log"]
|
"utils.threads" = ["utils", "macros", "dep:log"]
|
||||||
|
|
||||||
|
# tokio
|
||||||
|
"tokio.rt" = [
|
||||||
|
"tokio?/rt"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# default
|
# default
|
||||||
@ -95,3 +100,18 @@ required-features = ["sched.threads", "sched.rule-recurrence"]
|
|||||||
name = "sched_tokio"
|
name = "sched_tokio"
|
||||||
path = "examples/sched_tokio.rs"
|
path = "examples/sched_tokio.rs"
|
||||||
required-features = ["sched.tokio", "sched.rule-recurrence"]
|
required-features = ["sched.tokio", "sched.rule-recurrence"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "widget_text_area"
|
||||||
|
path = "examples/widget_text_area.rs"
|
||||||
|
required-features = ["cli.tui.widgets", "tokio.rt"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "widget_grid_selector"
|
||||||
|
path = "examples/widget_grid_selector.rs"
|
||||||
|
required-features = ["cli.tui.widgets", "tokio.rt"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "widget_switch"
|
||||||
|
path = "examples/widget_switch.rs"
|
||||||
|
required-features = ["cli.tui.widgets", "tokio.rt"]
|
||||||
|
|||||||
@ -29,6 +29,16 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- cargo run --example=threadpool --release --features utils.threads
|
- cargo run --example=threadpool --release --features utils.threads
|
||||||
|
|
||||||
|
example:gridselector:
|
||||||
|
desc: 🚀 run lool «example widget_grid_selector»
|
||||||
|
cmds:
|
||||||
|
- cargo watch --features=cli.tui.widgets,tokio.rt -c -x "run --example widget_grid_selector"
|
||||||
|
|
||||||
|
example:switch:
|
||||||
|
desc: 🚀 run lool «example widget_switch»
|
||||||
|
cmds:
|
||||||
|
- cargo watch --features=cli.tui.widgets,tokio.rt -c -x "run --example widget_switch"
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
desc: 🎨 format lool
|
desc: 🎨 format lool
|
||||||
cmds:
|
cmds:
|
||||||
|
|||||||
187
examples/widget_grid_selector.rs
Normal file
187
examples/widget_grid_selector.rs
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
use std::io;
|
||||||
|
|
||||||
|
use {
|
||||||
|
lool::tui::widgets::gridselector::GridItem,
|
||||||
|
ratatui::{
|
||||||
|
layout::{Alignment, Constraint, Direction, Layout},
|
||||||
|
style::{Modifier, Style, Stylize},
|
||||||
|
widgets::Paragraph,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use {
|
||||||
|
crossterm::{
|
||||||
|
event::{self, Event, KeyCode, KeyEvent, KeyEventKind},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
},
|
||||||
|
lool::tui::widgets::gridselector::{GridSelector, GridSelectorState},
|
||||||
|
ratatui::{layout::Rect, prelude::CrosstermBackend, style::Color, Frame, Terminal},
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Item {
|
||||||
|
name: String,
|
||||||
|
emoji: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
fn new(name: &str, emoji: &str) -> Self {
|
||||||
|
Item {
|
||||||
|
name: name.to_string(),
|
||||||
|
emoji: emoji.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Item> for GridItem {
|
||||||
|
fn from(val: Item) -> Self {
|
||||||
|
GridItem::new(format!("{} {}", val.name, val.emoji))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let mut stdout = stdout.lock();
|
||||||
|
enable_raw_mode()?;
|
||||||
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut term = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
// Run the draw loop
|
||||||
|
run(&mut term)?;
|
||||||
|
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(term.backend_mut(), LeaveAlternateScreen)?;
|
||||||
|
term.show_cursor()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run<B>(term: &mut Terminal<B>) -> io::Result<()>
|
||||||
|
where
|
||||||
|
B: ratatui::backend::Backend,
|
||||||
|
{
|
||||||
|
// vec![
|
||||||
|
// "feat ✨".to_string(),
|
||||||
|
// "fix".to_string(),
|
||||||
|
// "chore".to_string(),
|
||||||
|
// "docs".to_string(),
|
||||||
|
// "style".to_string(),
|
||||||
|
// "refactor".to_string(),
|
||||||
|
// "perf".to_string(),
|
||||||
|
// "test".to_string(),
|
||||||
|
// "build".to_string(),
|
||||||
|
// "ci".to_string(),
|
||||||
|
// "revert".to_string(),
|
||||||
|
// "release".to_string(),
|
||||||
|
// "wip".to_string(),
|
||||||
|
// ]
|
||||||
|
let items = vec![
|
||||||
|
Item::new("feat", "✨"),
|
||||||
|
Item::new("fix", "🐛"),
|
||||||
|
Item::new("chore", "🧹"),
|
||||||
|
Item::new("docs", "📚"),
|
||||||
|
Item::new("style", "💅"),
|
||||||
|
Item::new("refactor", "🔨"),
|
||||||
|
Item::new("perf", "🐎"),
|
||||||
|
Item::new("test", "🚨"),
|
||||||
|
Item::new("build", "👷"),
|
||||||
|
Item::new("ci", "🔧"),
|
||||||
|
Item::new("revert", "⏪"),
|
||||||
|
Item::new("release", "🚀"),
|
||||||
|
Item::new("wip", "🚧"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut grid_state = GridSelectorState::new(items).columns(5);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
term.draw(|f| draw(f, f.area(), &mut grid_state))?;
|
||||||
|
|
||||||
|
match event::read()? {
|
||||||
|
Event::Key(key) => match key {
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Esc, ..
|
||||||
|
} => break,
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Right,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
..
|
||||||
|
} => grid_state.move_right(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Left,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
..
|
||||||
|
} => grid_state.move_left(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Up,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
..
|
||||||
|
} => grid_state.move_up(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Down,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
..
|
||||||
|
} => grid_state.move_down(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Home,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
..
|
||||||
|
} => grid_state.move_to_row_start(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::End,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
..
|
||||||
|
} => grid_state.move_to_row_end(),
|
||||||
|
// selection
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Enter,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
..
|
||||||
|
} => grid_state.select(),
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(f: &mut Frame<'_>, area: Rect, state: &mut GridSelectorState) {
|
||||||
|
// vertical layout with 2 rows
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(2),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(2),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
f.render_stateful_widget(
|
||||||
|
GridSelector::default().with_selected_color(Color::Magenta),
|
||||||
|
layout[0],
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
|
||||||
|
let selected = state.selected().unwrap_or(GridItem::new("None"));
|
||||||
|
let hovered = state.hovered().unwrap_or(GridItem::new("None"));
|
||||||
|
|
||||||
|
let ps = Paragraph::new(selected)
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.style(Style::default().fg(Color::Magenta));
|
||||||
|
|
||||||
|
let ph = Paragraph::new(hovered)
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.style(Style::default().fg(Color::Blue));
|
||||||
|
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new("Hovered/Selected")
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
layout[1],
|
||||||
|
);
|
||||||
|
f.render_widget(ph, layout[2]);
|
||||||
|
f.render_widget(ps, layout[3]);
|
||||||
|
}
|
||||||
80
examples/widget_switch.rs
Normal file
80
examples/widget_switch.rs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
use {
|
||||||
|
lool::tui::{
|
||||||
|
ratatui::{
|
||||||
|
backend::CrosstermBackend,
|
||||||
|
crossterm::{
|
||||||
|
event::{self},
|
||||||
|
execute,
|
||||||
|
terminal::{
|
||||||
|
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout::{Constraint, Layout},
|
||||||
|
Terminal,
|
||||||
|
},
|
||||||
|
widgets::{
|
||||||
|
switch::Switch,
|
||||||
|
textarea::{Input, Key},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ratatui::layout::Flex,
|
||||||
|
std::io,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let mut stdout = stdout.lock();
|
||||||
|
|
||||||
|
enable_raw_mode()?;
|
||||||
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut term = Terminal::new(backend)?;
|
||||||
|
let mut switch_state = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
term.draw(|f| {
|
||||||
|
// Render the textarea
|
||||||
|
let switch =
|
||||||
|
Switch::with_status(switch_state).with_color_on(ratatui::style::Color::Blue);
|
||||||
|
|
||||||
|
let [horiz] = Layout::horizontal([Constraint::Percentage(100)])
|
||||||
|
.flex(Flex::Center)
|
||||||
|
.areas(f.area());
|
||||||
|
|
||||||
|
let [verti] = Layout::vertical([Constraint::Length(2)]).flex(Flex::Center).areas(horiz);
|
||||||
|
|
||||||
|
let [centered] =
|
||||||
|
Layout::horizontal([Constraint::Length(14)]).flex(Flex::Center).areas(verti);
|
||||||
|
|
||||||
|
f.render_widget(switch, centered);
|
||||||
|
})?;
|
||||||
|
match event::read()?.into() {
|
||||||
|
Input { key: Key::Esc, .. }
|
||||||
|
| Input {
|
||||||
|
key: Key::Char('c'),
|
||||||
|
shift: false,
|
||||||
|
ctrl: true,
|
||||||
|
alt: false,
|
||||||
|
} => break,
|
||||||
|
Input {
|
||||||
|
key: Key::Enter,
|
||||||
|
ctrl: false,
|
||||||
|
shift: false,
|
||||||
|
alt: false,
|
||||||
|
}
|
||||||
|
| Input {
|
||||||
|
key: Key::Char(' '),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
switch_state = !switch_state;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(term.backend_mut(), LeaveAlternateScreen)?;
|
||||||
|
term.show_cursor()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
77
examples/widget_text_area.rs
Normal file
77
examples/widget_text_area.rs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
use {
|
||||||
|
lool::tui::{
|
||||||
|
ratatui::{
|
||||||
|
backend::CrosstermBackend,
|
||||||
|
crossterm::{
|
||||||
|
event::{self},
|
||||||
|
execute,
|
||||||
|
terminal::{
|
||||||
|
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout::{Constraint, Direction, Layout},
|
||||||
|
Terminal,
|
||||||
|
},
|
||||||
|
widgets::textarea::{Input, Key, TextArea},
|
||||||
|
},
|
||||||
|
ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph},
|
||||||
|
std::{cmp, io},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let mut stdout = stdout.lock();
|
||||||
|
|
||||||
|
enable_raw_mode()?;
|
||||||
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut term = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
let mut textarea = TextArea::default().with_block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.title(" Your name ")
|
||||||
|
.padding(Padding::horizontal(1)),
|
||||||
|
);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
term.draw(|f| {
|
||||||
|
const MIN_HEIGHT: usize = 1;
|
||||||
|
let height = cmp::max(textarea.lines().len(), MIN_HEIGHT) as u16 + 2;
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(height), Constraint::Min(1)])
|
||||||
|
.split(f.area());
|
||||||
|
|
||||||
|
// Render the textarea
|
||||||
|
f.render_widget(&textarea, layout[0]);
|
||||||
|
f.render_widget(Paragraph::new("Press <Esc> to exit"), layout[1]);
|
||||||
|
})?;
|
||||||
|
match event::read()?.into() {
|
||||||
|
Input { key: Key::Esc, .. }
|
||||||
|
| Input {
|
||||||
|
key: Key::Char('c'),
|
||||||
|
shift: false,
|
||||||
|
ctrl: true,
|
||||||
|
alt: false,
|
||||||
|
}
|
||||||
|
| Input {
|
||||||
|
key: Key::Enter,
|
||||||
|
ctrl: false,
|
||||||
|
shift: false,
|
||||||
|
alt: false,
|
||||||
|
} => break,
|
||||||
|
input => {
|
||||||
|
textarea.input(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(term.backend_mut(), LeaveAlternateScreen)?;
|
||||||
|
term.show_cursor()?;
|
||||||
|
|
||||||
|
println!("Lines: {:?}", textarea.lines());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -209,3 +209,47 @@ textarea..with_validations(vec![
|
|||||||
required_validator,
|
required_validator,
|
||||||
]);
|
]);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `GridSelector` Widget
|
||||||
|
|
||||||
|
A selector restful widget that can be used to select items from a list. The items are displayed in a
|
||||||
|
grid, and the user can navigate through them using, for example, the arrow keys.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
See the [`widget_grid_selector.rs`](/examples/widget_grid_selector.rs) example for a full
|
||||||
|
demonstration of how to use the `GridSelector` widget.
|
||||||
|
|
||||||
|
In summary, the widget uses a `GridSelectorState` to keep track of the items and the state of the
|
||||||
|
widget.
|
||||||
|
|
||||||
|
The `GridSelectorState` takes a list of `GridItem`, which is a basic struct that encapsulates a
|
||||||
|
`String` value.
|
||||||
|
|
||||||
|
The `GridSelectorState::new` method accepts a list of any type that can be converted to a
|
||||||
|
`GridItem`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn new<I, T>(items: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = T>,
|
||||||
|
T: Into<GridItem>, // Accept anything that can be converted into GridItem
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As a part of this library, the `Into<GridItem>` trait is implemented for `String`, `&str`.
|
||||||
|
|
||||||
|
The example at [`widget_grid_selector.rs`](/examples/widget_grid_selector.rs) demonstrates how to
|
||||||
|
implement the `Into<GridItem>` trait for a custom type.
|
||||||
|
|
||||||
|
## `Switch` Widget
|
||||||
|
|
||||||
|
A simple stateless switch widget that can be used to show visual feedback of a boolean state.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
See the [`widget_switch.rs`](/examples/widget_switch.rs) example for a full demonstration of how to
|
||||||
|
use the `Switch` widget.
|
||||||
@ -261,6 +261,7 @@ pub trait Component: Downcast {
|
|||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// * `Option<&Box<dyn Component>>` - A reference to the child component or none.
|
/// * `Option<&Box<dyn Component>>` - A reference to the child component or none.
|
||||||
|
#[allow(clippy::borrowed_box)]
|
||||||
fn child(&mut self, name: &str) -> Option<&Box<dyn Component>> {
|
fn child(&mut self, name: &str) -> Option<&Box<dyn Component>> {
|
||||||
if let Some(children) = self.get_children() {
|
if let Some(children) = self.get_children() {
|
||||||
children.get(name)
|
children.get(name)
|
||||||
|
|||||||
@ -31,7 +31,20 @@ pub mod utils {
|
|||||||
|
|
||||||
#[cfg(feature = "cli.tui.widgets")]
|
#[cfg(feature = "cli.tui.widgets")]
|
||||||
pub mod widgets {
|
pub mod widgets {
|
||||||
|
pub mod gridselector {
|
||||||
|
mod selector;
|
||||||
|
mod state;
|
||||||
|
mod widget;
|
||||||
|
|
||||||
|
pub use {selector::*, state::*};
|
||||||
|
}
|
||||||
|
|
||||||
pub mod textarea;
|
pub mod textarea;
|
||||||
|
|
||||||
|
pub mod switch {
|
||||||
|
mod widget;
|
||||||
|
pub use widget::*;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ratatui prelude
|
// ratatui prelude
|
||||||
|
|||||||
68
lib/cli/tui/widgets/gridselector/selector.rs
Normal file
68
lib/cli/tui/widgets/gridselector/selector.rs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
//! {`GridSelector`} widget
|
||||||
|
//!
|
||||||
|
//! This module contains the [`BoxSelector`] ratatui widget.
|
||||||
|
//!
|
||||||
|
//! The grid selector is a stateful widget that allows the user to select an item from a list of
|
||||||
|
//! items displayed in a grid. The user can navigate the grid with the arrow keys, and select the
|
||||||
|
//! currently hovered item with the `Enter` key.
|
||||||
|
//!
|
||||||
|
//! The [`GridSelector`] widget uses a [`GridSelectorState`] to be able to keep its state between
|
||||||
|
//! renders.
|
||||||
|
|
||||||
|
use {super::GridSelectorState, ratatui::style::Color};
|
||||||
|
|
||||||
|
pub struct GridSelector {
|
||||||
|
color: Color,
|
||||||
|
hovered_color: Color,
|
||||||
|
selected_color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GridSelector {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
color: Color::Reset,
|
||||||
|
hovered_color: Color::Blue,
|
||||||
|
selected_color: Color::Green,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// imlementation of the build pattern for the GridSelector to set the colors
|
||||||
|
|
||||||
|
impl GridSelector {
|
||||||
|
/// Set the color of the items in the grid.
|
||||||
|
pub fn with_color(mut self, color: Color) -> Self {
|
||||||
|
self.color = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the color of the hovered item in the grid.
|
||||||
|
pub fn with_hovered_color(mut self, color: Color) -> Self {
|
||||||
|
self.hovered_color = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the color of the selected item in the grid.
|
||||||
|
pub fn with_selected_color(mut self, color: Color) -> Self {
|
||||||
|
self.selected_color = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_color(&self, for_idx: usize, state: &GridSelectorState) -> Color {
|
||||||
|
let mut color = self.color;
|
||||||
|
|
||||||
|
if let Some(hovered_idx) = state.hovered {
|
||||||
|
if for_idx == hovered_idx {
|
||||||
|
color = self.hovered_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(selected_index) = state.selected {
|
||||||
|
if for_idx == selected_index {
|
||||||
|
color = self.selected_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
272
lib/cli/tui/widgets/gridselector/state.rs
Normal file
272
lib/cli/tui/widgets/gridselector/state.rs
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
//! # Grid Selector State
|
||||||
|
//!
|
||||||
|
//! This module contains the [`GridSelectorState`] for the `GridSelector` widget.
|
||||||
|
//!
|
||||||
|
//! The state is used to keep track of the items, the selected item, and the hovered item and
|
||||||
|
//! encapsulates the navigation logic for the grid selector.
|
||||||
|
|
||||||
|
use ratatui::text::Text;
|
||||||
|
|
||||||
|
// If Text has a lifetime parameter, specify it in the implementation.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GridItem(String);
|
||||||
|
|
||||||
|
impl GridItem {
|
||||||
|
// accept both String and &str
|
||||||
|
pub fn new<S>(value: S) -> Self
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
GridItem(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert Label into &str
|
||||||
|
impl AsRef<str> for GridItem {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert Label into String
|
||||||
|
impl From<GridItem> for String {
|
||||||
|
fn from(val: GridItem) -> Self {
|
||||||
|
val.0.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify the lifetime for the implementation
|
||||||
|
impl<'a> From<GridItem> for Text<'a> {
|
||||||
|
fn from(val: GridItem) -> Self {
|
||||||
|
Text::from(val.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// implement a way to convert String into Label
|
||||||
|
impl From<String> for GridItem {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
GridItem(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// implement a way to convert &str into Label
|
||||||
|
impl<'a> From<&'a str> for GridItem {
|
||||||
|
fn from(value: &'a str) -> Self {
|
||||||
|
GridItem(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State for the [`GridSelector`] widget.
|
||||||
|
///
|
||||||
|
/// This state is used to keep track of the items, the selected item, and the hovered item.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GridSelectorState {
|
||||||
|
pub items: Vec<GridItem>,
|
||||||
|
pub selected: Option<usize>,
|
||||||
|
pub hovered: Option<usize>,
|
||||||
|
pub(crate) columns: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GridSelectorState {
|
||||||
|
/// Create a new [`GridSelectorState`] with the given items.
|
||||||
|
pub fn new<I, T>(items: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = T>,
|
||||||
|
T: Into<GridItem>, // Accept anything that can be converted into Label
|
||||||
|
{
|
||||||
|
let items: Vec<GridItem> = items.into_iter().map(Into::into).collect();
|
||||||
|
Self {
|
||||||
|
items,
|
||||||
|
selected: None,
|
||||||
|
hovered: Some(0),
|
||||||
|
columns: 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// builder method to set the number of columns
|
||||||
|
pub fn columns(mut self, columns: usize) -> Self {
|
||||||
|
self.columns = columns;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the selected item.
|
||||||
|
pub fn selected(&self) -> Option<GridItem> {
|
||||||
|
self.selected.map(|i| self.items[i].clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the index of the selected item.
|
||||||
|
pub fn selected_index(&self) -> Option<usize> {
|
||||||
|
self.selected
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the hovered item.
|
||||||
|
pub fn hovered(&self) -> Option<GridItem> {
|
||||||
|
self.hovered.map(|i| self.items[i].clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the hovered item right +1
|
||||||
|
///
|
||||||
|
/// Returns `true` if the hovered item was moved, `false` otherwise.
|
||||||
|
pub fn move_right(&mut self) -> bool {
|
||||||
|
self.hovered = if let Some(hovered) = self.hovered {
|
||||||
|
let next = hovered + 1;
|
||||||
|
if next < self.items.len() {
|
||||||
|
Some(next)
|
||||||
|
} else {
|
||||||
|
Some(0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(0)
|
||||||
|
};
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the hovered item left -1
|
||||||
|
///
|
||||||
|
/// Returns `true` if the hovered item was moved,
|
||||||
|
/// `false` otherwise.
|
||||||
|
pub fn move_left(&mut self) -> bool {
|
||||||
|
self.hovered = if let Some(hovered) = self.hovered {
|
||||||
|
if hovered > 0 {
|
||||||
|
Some(hovered - 1)
|
||||||
|
} else {
|
||||||
|
Some(self.items.len() - 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(0)
|
||||||
|
};
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the hovered item down by one row.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the hovered item was moved, `false` otherwise.
|
||||||
|
pub fn move_down(&mut self) -> bool {
|
||||||
|
if let Some(hovered) = self.hovered {
|
||||||
|
let items_per_row = self.columns;
|
||||||
|
let num_items = self.items.len();
|
||||||
|
let current_row = hovered / items_per_row;
|
||||||
|
let next_row_start = (current_row + 1) * items_per_row;
|
||||||
|
let last_item_index = num_items - 1;
|
||||||
|
|
||||||
|
// If we are in the last row, we can't go down
|
||||||
|
if next_row_start > last_item_index {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut next_index = std::cmp::min(hovered + items_per_row, last_item_index);
|
||||||
|
|
||||||
|
// Handle the case where the next row has fewer items
|
||||||
|
let next_row_count = std::cmp::min(items_per_row, last_item_index - next_row_start + 1);
|
||||||
|
|
||||||
|
// check if both the next_row_count and the self.columns are odd numbers (3,5,7, etc)
|
||||||
|
// and next_row_count is less than self.columns
|
||||||
|
if next_row_count % 2 != 0 && items_per_row % 2 != 0 && next_row_count < items_per_row {
|
||||||
|
let shift = (items_per_row - next_row_count) / 2;
|
||||||
|
// if we are in the shifted range (left or right) of the row, adjust the hovered
|
||||||
|
// index accordingly
|
||||||
|
if hovered % items_per_row >= shift
|
||||||
|
&& hovered % items_per_row < items_per_row - shift
|
||||||
|
{
|
||||||
|
next_index = hovered + items_per_row - shift;
|
||||||
|
} else {
|
||||||
|
// if we are in the left part of the shifted range, set next to the first item
|
||||||
|
// in the last row and if we are in the right part of the shifted range, set
|
||||||
|
// next to the last item in the last row
|
||||||
|
next_index = if hovered % items_per_row < shift {
|
||||||
|
next_row_start
|
||||||
|
} else {
|
||||||
|
last_item_index
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.hovered = Some(std::cmp::min(next_index, last_item_index));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the hovered item up by one row.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the hovered item was moved, `false` otherwise.
|
||||||
|
pub fn move_up(&mut self) -> bool {
|
||||||
|
if let Some(hovered) = self.hovered {
|
||||||
|
let row_number = hovered / self.columns;
|
||||||
|
|
||||||
|
// If we are in the first row, we can't go up
|
||||||
|
if row_number == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut next_index = hovered.saturating_sub(self.columns);
|
||||||
|
|
||||||
|
// Handle case where the current index is in the last row
|
||||||
|
// let is_last_row = hovered >= self.items.len().saturating_sub(self.columns);
|
||||||
|
let last_row_start = (self.items.len() / self.columns) * self.columns;
|
||||||
|
let is_last_row = hovered >= last_row_start;
|
||||||
|
|
||||||
|
if is_last_row {
|
||||||
|
let last_row_count = self.items.len() % self.columns;
|
||||||
|
|
||||||
|
// If the last_row_count and self.columns are odd numbers (3,5,7, etc)
|
||||||
|
// and last_row_count is less than self.columns we need to adjust the next index
|
||||||
|
// to go to the cell just above the current hovered cell
|
||||||
|
if last_row_count % 2 != 0 && self.columns % 2 != 0 && last_row_count < self.columns
|
||||||
|
{
|
||||||
|
let shift = (self.columns - last_row_count) / 2;
|
||||||
|
next_index = hovered.saturating_sub(self.columns - shift);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure next_index stays within bounds
|
||||||
|
self.hovered = Some(std::cmp::min(next_index, self.items.len() - 1));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the hovered item to the first item in the current row.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the hovered item was moved, `false` otherwise.
|
||||||
|
pub fn move_to_row_start(&mut self) -> bool {
|
||||||
|
if let Some(hovered) = self.hovered {
|
||||||
|
let row_start = (hovered / self.columns) * self.columns;
|
||||||
|
self.hovered = Some(row_start);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the hovered item to the last item in the current row.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the hovered item was moved, `false` otherwise.
|
||||||
|
pub fn move_to_row_end(&mut self) -> bool {
|
||||||
|
if let Some(hovered) = self.hovered {
|
||||||
|
let row_end = std::cmp::min(
|
||||||
|
(hovered / self.columns + 1) * self.columns - 1,
|
||||||
|
self.items.len() - 1,
|
||||||
|
);
|
||||||
|
self.hovered = Some(row_end);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select the hovered item.
|
||||||
|
///
|
||||||
|
/// Select the hovered item. Returns `true` if the hovered item was selected, `false` otherwise.
|
||||||
|
pub fn select(&mut self) -> bool {
|
||||||
|
if let Some(hovered) = self.hovered {
|
||||||
|
self.selected = Some(hovered);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
lib/cli/tui/widgets/gridselector/widget.rs
Normal file
63
lib/cli/tui/widgets/gridselector/widget.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use {
|
||||||
|
super::{GridSelector, GridSelectorState},
|
||||||
|
ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Alignment, Constraint, Direction, Flex, Layout, Rect},
|
||||||
|
style::Style,
|
||||||
|
widgets::{Block, Borders, Paragraph, StatefulWidget, Widget},
|
||||||
|
},
|
||||||
|
std::rc::Rc,
|
||||||
|
unicode_width::UnicodeWidthStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl StatefulWidget for GridSelector {
|
||||||
|
type State = GridSelectorState;
|
||||||
|
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut GridSelectorState) {
|
||||||
|
let rows_layout = rows_layout(state, area);
|
||||||
|
let largest_item = largest_item(state);
|
||||||
|
|
||||||
|
for (i, row) in rows_layout.iter().enumerate() {
|
||||||
|
let row_items = state.items.iter().skip(i * state.columns).take(state.columns);
|
||||||
|
let columns_layout = columns_layout(row, row_items.len(), largest_item);
|
||||||
|
|
||||||
|
for (j, item) in row_items.enumerate() {
|
||||||
|
let main_index = i * state.columns + j;
|
||||||
|
let color = self.get_color(main_index, state);
|
||||||
|
|
||||||
|
let type_block =
|
||||||
|
Block::default().borders(Borders::ALL).border_style(Style::default().fg(color));
|
||||||
|
|
||||||
|
Paragraph::new(item.clone())
|
||||||
|
.style(Style::default().fg(color))
|
||||||
|
.alignment(Alignment::Left)
|
||||||
|
.block(type_block)
|
||||||
|
.render(columns_layout[j], buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rows_layout(state: &GridSelectorState, area: Rect) -> Rc<[Rect]> {
|
||||||
|
let row_count = (state.items.len() as f32 / state.columns as f32).ceil() as usize;
|
||||||
|
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints((0..row_count).map(|_| Constraint::Length(3)).collect::<Vec<_>>())
|
||||||
|
.spacing(0)
|
||||||
|
.split(area)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn columns_layout(row: &Rect, row_item_count: usize, largest_item: u16) -> Rc<[Rect]> {
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.flex(Flex::Center)
|
||||||
|
.constraints(
|
||||||
|
(0..row_item_count).map(|_| Constraint::Length(largest_item + 3)).collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.split(*row)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn largest_item(state: &GridSelectorState) -> u16 {
|
||||||
|
state.items.iter().map(|item| UnicodeWidthStr::width(item.as_ref())).max().unwrap_or(0) as u16
|
||||||
|
}
|
||||||
88
lib/cli/tui/widgets/switch/widget.rs
Normal file
88
lib/cli/tui/widgets/switch/widget.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Layout, Rect},
|
||||||
|
style::{Color, Style},
|
||||||
|
widgets::{Block, Widget},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A switch widget
|
||||||
|
///
|
||||||
|
/// This widget is used to show visual confirmation of a boolean state
|
||||||
|
pub struct Switch {
|
||||||
|
/// The state of the switch
|
||||||
|
state: bool,
|
||||||
|
/// The color of the "on" state (`Green` by default)
|
||||||
|
color_on: Color,
|
||||||
|
/// The color of the "off" state (`DarkGray` by default)
|
||||||
|
color_off: Color,
|
||||||
|
/// The color of the switch itself (`White` by default)
|
||||||
|
color_switch: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Switch {
|
||||||
|
pub fn with_status(state: bool) -> Self {
|
||||||
|
Switch {
|
||||||
|
state,
|
||||||
|
color_on: Color::Green,
|
||||||
|
color_off: Color::DarkGray,
|
||||||
|
color_switch: Color::White,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_color_on(mut self, color: Color) -> Self {
|
||||||
|
self.color_on = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_color_off(mut self, color: Color) -> Self {
|
||||||
|
self.color_off = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_color_switch(mut self, color: Color) -> Self {
|
||||||
|
self.color_switch = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_layout(&self, area: Rect) -> (Rect, Rect) {
|
||||||
|
let main = Layout::default()
|
||||||
|
.direction(ratatui::layout::Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(2)])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(ratatui::layout::Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Length(7), Constraint::Length(7)])
|
||||||
|
.split(main[0]);
|
||||||
|
|
||||||
|
(layout[0], layout[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_left_color(&self) -> Color {
|
||||||
|
if self.state {
|
||||||
|
self.color_on
|
||||||
|
} else {
|
||||||
|
self.color_switch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_right_color(&self) -> Color {
|
||||||
|
if self.state {
|
||||||
|
self.color_switch
|
||||||
|
} else {
|
||||||
|
self.color_off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Switch {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let (left, right) = self.get_layout(area);
|
||||||
|
|
||||||
|
let left_block = Block::default().style(Style::default().bg(self.get_left_color()));
|
||||||
|
let right_block = Block::default().style(Style::default().bg(self.get_right_color()));
|
||||||
|
|
||||||
|
left_block.render(left, buf);
|
||||||
|
right_block.render(right, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
use {
|
use {
|
||||||
super::util::{find_word_start_backward, find_word_start_forward},
|
super::util::{find_word_start_backward, find_word_start_forward},
|
||||||
crate::tui::widgets::textarea::textarea::widget::Viewport,
|
crate::tui::widgets::textarea::core::widget::Viewport,
|
||||||
std::cmp,
|
std::cmp,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
|||||||
/// Backend-agnostic key input kind.
|
/// Backend-agnostic key input kind.
|
||||||
///
|
///
|
||||||
/// This type is marked as `#[non_exhaustive]` since more keys may be supported in the future.
|
/// This type is marked as `#[non_exhaustive]` since more keys may be supported in the future.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, Default)]
|
||||||
pub enum Key {
|
pub enum Key {
|
||||||
/// Normal letter key input
|
/// Normal letter key input
|
||||||
Char(char),
|
Char(char),
|
||||||
@ -38,15 +38,10 @@ pub enum Key {
|
|||||||
/// Paste key. This key is supported by termwiz only
|
/// Paste key. This key is supported by termwiz only
|
||||||
Paste,
|
Paste,
|
||||||
/// An invalid key input (this key is always ignored by [`TextArea`](crate::TextArea))
|
/// An invalid key input (this key is always ignored by [`TextArea`](crate::TextArea))
|
||||||
|
#[default]
|
||||||
Null,
|
Null,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Key {
|
|
||||||
fn default() -> Self {
|
|
||||||
Key::Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Backend-agnostic key input type.
|
/// Backend-agnostic key input type.
|
||||||
///
|
///
|
||||||
/// When `crossterm`, `termion`, `termwiz` features are enabled, converting respective key input types into this
|
/// When `crossterm`, `termion`, `termwiz` features are enabled, converting respective key input types into this
|
||||||
@ -104,7 +99,7 @@ impl Input {
|
|||||||
ctrl: false,
|
ctrl: false,
|
||||||
alt: false,
|
alt: false,
|
||||||
..
|
..
|
||||||
} => Some(c.clone()),
|
} => Some(*c),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,30 +112,27 @@ impl Input {
|
|||||||
/// - Char is \n or \r
|
/// - Char is \n or \r
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_newline_except_enter(&self) -> bool {
|
pub fn is_newline_except_enter(&self) -> bool {
|
||||||
match self {
|
matches!(
|
||||||
|
self,
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('\n' | '\r'),
|
key: Key::Char('\n' | '\r'),
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
alt: false,
|
alt: false,
|
||||||
..
|
..
|
||||||
}
|
} | Input {
|
||||||
| Input {
|
|
||||||
key: Key::Enter,
|
key: Key::Enter,
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
..
|
..
|
||||||
}
|
} | Input {
|
||||||
| Input {
|
|
||||||
key: Key::Enter,
|
key: Key::Enter,
|
||||||
alt: true,
|
alt: true,
|
||||||
..
|
..
|
||||||
}
|
} | Input {
|
||||||
| Input {
|
|
||||||
key: Key::Enter,
|
key: Key::Enter,
|
||||||
shift: true,
|
shift: true,
|
||||||
..
|
..
|
||||||
} => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the Input represents a new line (including Enter key).
|
/// Returns `true` if the Input represents a new line (including Enter key).
|
||||||
@ -170,67 +162,67 @@ impl Input {
|
|||||||
/// Returns `true` if the Input is a Tab
|
/// Returns `true` if the Input is a Tab
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_tab(&self) -> bool {
|
pub fn is_tab(&self) -> bool {
|
||||||
return self.key == Key::Tab && !self.ctrl && !self.alt;
|
self.key == Key::Tab && !self.ctrl && !self.alt
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the Input is Backspace
|
/// Returns `true` if the Input is Backspace
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_backspace(&self) -> bool {
|
pub fn is_backspace(&self) -> bool {
|
||||||
return self.key == Key::Backspace && !self.ctrl && !self.alt;
|
self.key == Key::Backspace && !self.ctrl && !self.alt
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the Input is Delete
|
/// Returns `true` if the Input is Delete
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_delete(&self) -> bool {
|
pub fn is_delete(&self) -> bool {
|
||||||
return self.key == Key::Delete && !self.ctrl && !self.alt;
|
self.key == Key::Delete && !self.ctrl && !self.alt
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the Input is key down arrow
|
/// Returns `true` if the Input is key down arrow
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_down(&self) -> bool {
|
pub fn is_down(&self) -> bool {
|
||||||
return self.key == Key::Down && !self.ctrl && !self.alt;
|
self.key == Key::Down && !self.ctrl && !self.alt
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the Input is key up arrow
|
/// Returns `true` if the Input is key up arrow
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_up(&self) -> bool {
|
pub fn is_up(&self) -> bool {
|
||||||
return self.key == Key::Up && !self.ctrl && !self.alt;
|
self.key == Key::Up && !self.ctrl && !self.alt
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the Input is key left arrow
|
/// Returns `true` if the Input is key left arrow
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_left(&self) -> bool {
|
pub fn is_left(&self) -> bool {
|
||||||
return self.key == Key::Left && !self.ctrl && !self.alt;
|
self.key == Key::Left && !self.ctrl && !self.alt
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the Input is key right arrow
|
/// Returns `true` if the Input is key right arrow
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_right(&self) -> bool {
|
pub fn is_right(&self) -> bool {
|
||||||
return self.key == Key::Right && !self.ctrl && !self.alt;
|
self.key == Key::Right && !self.ctrl && !self.alt
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the Input is key Home
|
/// Returns `true` if the Input is key Home
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_home(&self) -> bool {
|
pub fn is_home(&self) -> bool {
|
||||||
return self.key == Key::Home;
|
self.key == Key::Home
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the Input is key End
|
/// Returns `true` if the Input is key End
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_end(&self) -> bool {
|
pub fn is_end(&self) -> bool {
|
||||||
return self.key == Key::End;
|
self.key == Key::End
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the Input is ctrl+left
|
/// Returns `true` if the Input is ctrl+left
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_ctrl_left(&self) -> bool {
|
pub fn is_ctrl_left(&self) -> bool {
|
||||||
return self.key == Key::Left && self.ctrl && !self.alt;
|
self.key == Key::Left && self.ctrl && !self.alt
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the Input is ctrl+right
|
/// Returns `true` if the Input is ctrl+right
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_ctrl_right(&self) -> bool {
|
pub fn is_ctrl_right(&self) -> bool {
|
||||||
return self.key == Key::Right && self.ctrl && !self.alt;
|
self.key == Key::Right && self.ctrl && !self.alt
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a string representing the kind of key input.
|
/// Returns a string representing the kind of key input.
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::tui::widgets::textarea::textarea::widget::Viewport;
|
use crate::tui::widgets::textarea::core::widget::Viewport;
|
||||||
|
|
||||||
/// Specify how to scroll the textarea.
|
/// Specify how to scroll the textarea.
|
||||||
///
|
///
|
||||||
|
|||||||
@ -59,5 +59,5 @@ pub fn find_word_start_backward(line: &str, start_col: usize) -> Option<usize> {
|
|||||||
}
|
}
|
||||||
cur = next;
|
cur = next;
|
||||||
}
|
}
|
||||||
(cur != CharKind::Space).then(|| 0)
|
(cur != CharKind::Space).then_some(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -512,5 +512,3 @@ impl<'a> TextArea<'a> {
|
|||||||
hl.into_spans()
|
hl.into_spans()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builder-Pattern methods
|
|
||||||
@ -7,8 +7,10 @@ pub enum ValidationResult {
|
|||||||
Invalid(Vec<String>),
|
Invalid(Vec<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ValidatorFnType = Arc<dyn Fn(&str) -> Result<(), String> + Send + Sync>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ValidatorFn(Arc<dyn Fn(&str) -> Result<(), String> + Send + Sync>);
|
pub struct ValidatorFn(ValidatorFnType);
|
||||||
|
|
||||||
impl ValidatorFn {
|
impl ValidatorFn {
|
||||||
pub fn new<F>(f: F) -> Self
|
pub fn new<F>(f: F) -> Self
|
||||||
@ -1,6 +1,6 @@
|
|||||||
pub fn required_validator(input: &str) -> Result<(), String> {
|
pub fn required_validator(input: &str) -> Result<(), String> {
|
||||||
if input.is_empty() {
|
if input.is_empty() {
|
||||||
Err(format!("This field is required"))
|
Err("This field is required".to_string())
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -6,11 +6,11 @@ pub(super) mod behaviour {
|
|||||||
pub(super) mod util;
|
pub(super) mod util;
|
||||||
}
|
}
|
||||||
|
|
||||||
mod textarea;
|
mod core;
|
||||||
|
|
||||||
pub use {
|
pub use {
|
||||||
behaviour::input::{Input, Key},
|
behaviour::input::{Input, Key},
|
||||||
textarea::{
|
core::{
|
||||||
validation::{validators, ValidationResult},
|
validation::{validators, ValidationResult},
|
||||||
TextArea,
|
TextArea,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user