Implement form input w/ tui-realm-textarea

This commit is contained in:
Erik Kundt
2023-08-11 11:44:33 +02:00
parent 1d26f1612c
commit a827e22796
6 changed files with 282 additions and 49 deletions
Generated
+128
View File
@@ -300,6 +300,31 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossterm"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
dependencies = [
"bitflags 1.3.2",
"crossterm_winapi",
"libc",
"mio",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]]
name = "crypto-bigint"
version = "0.4.9"
@@ -918,6 +943,16 @@ dependencies = [
"serde",
]
[[package]]
name = "lock_api"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.19"
@@ -939,6 +974,18 @@ dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
]
[[package]]
name = "multibase"
version = "0.9.1"
@@ -1062,6 +1109,29 @@ dependencies = [
"sha2 0.10.7",
]
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.3.5",
"smallvec",
"windows-targets",
]
[[package]]
name = "pbkdf2"
version = "0.11.0"
@@ -1336,6 +1406,7 @@ dependencies = [
"textwrap 0.16.0",
"timeago",
"tui-realm-stdlib",
"tui-realm-textarea",
"tuirealm",
]
@@ -1522,6 +1593,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sec1"
version = "0.3.0"
@@ -1602,6 +1679,36 @@ dependencies = [
"keccak",
]
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "1.6.4"
@@ -1902,6 +2009,7 @@ checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
dependencies = [
"bitflags 1.3.2",
"cassowary",
"crossterm",
"termion 1.5.6",
"unicode-segmentation",
"unicode-width",
@@ -1918,6 +2026,26 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "tui-realm-textarea"
version = "1.1.1"
source = "git+https://github.com/erak/tui-realm-textarea.git?branch=no-borders#07f8344e0d7c6448abb9df96e900bac65b8856ad"
dependencies = [
"lazy-regex",
"tui-textarea",
"tuirealm",
]
[[package]]
name = "tui-textarea"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7a086dc7ddcec25bfb9c47d5f3d85e3313c0c67eebe645d9e6b883452e56cc"
dependencies = [
"crossterm",
"tui",
]
[[package]]
name = "tuirealm"
version = "1.8.0"
+1
View File
@@ -22,3 +22,4 @@ timeago = { version = "0.4.1" }
textwrap = { version = "0.16.0" }
tuirealm = { version = "1.8.0", default-features = false, features = [ "with-termion" ] }
tui-realm-stdlib = { version = "1.2.0", default-features = false, features = [ "with-termion" ] }
tui-realm-textarea = { git = "https://github.com/erak/tui-realm-textarea.git", branch = "no-borders", default-features = false, features = [ "with-termion" ] }
+21 -7
View File
@@ -6,6 +6,7 @@ use radicle_tui::ui::widget::common::container::{
AppHeader, GlobalListener, LabeledContainer, Popup,
};
use radicle_tui::ui::widget::common::context::{ContextBar, Shortcuts};
use radicle_tui::ui::widget::common::form::TextInput;
use radicle_tui::ui::widget::common::list::PropertyList;
use radicle_tui::ui::widget::home::{Dashboard, IssueBrowser, PatchBrowser};
use radicle_tui::ui::widget::{issue, patch};
@@ -148,12 +149,18 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<issue::NewForm> {
Some(Message::Tick)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
code: Key::Enter, ..
}) => {
self.perform(Cmd::Type(ch));
self.perform(Cmd::Custom(TextInput::CMD_NEWLINE));
Some(Message::Tick)
}
Event::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::ALT,
}) => {
self.perform(Cmd::Submit);
None
}
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Message::Issue(IssueMessage::ClosePopup(IssueCid::NewForm)))
}
@@ -168,11 +175,18 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<issue::NewForm> {
Some(Message::Tick)
}
Event::Keyboard(KeyEvent {
code: Key::Char('s'),
modifiers: KeyModifiers::ALT,
code: Key::Char(ch),
modifiers: KeyModifiers::SHIFT,
}) => {
self.perform(Cmd::Submit);
None
self.perform(Cmd::Type(ch.to_ascii_uppercase()));
Some(Message::Tick)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
..
}) => {
self.perform(Cmd::Type(ch));
Some(Message::Tick)
}
_ => None,
}
+3 -1
View File
@@ -439,7 +439,9 @@ impl ViewPage for IssuePage {
app.umount(&Cid::Issue(IssueCid::List))?;
app.umount(&Cid::Issue(IssueCid::Details))?;
app.umount(&Cid::Issue(IssueCid::Context))?;
app.umount(&Cid::Issue(IssueCid::NewForm))?;
if app.mounted(&Cid::Issue(IssueCid::NewForm)) {
app.umount(&Cid::Issue(IssueCid::NewForm))?;
}
app.umount(&Cid::Issue(IssueCid::Shortcuts))?;
Ok(())
}
+115 -9
View File
@@ -1,21 +1,94 @@
use tui_realm_stdlib::Input;
use tui_realm_textarea::TextArea;
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::tui::layout::{Constraint, Direction, Rect};
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State};
use tuirealm::props::Style;
use tuirealm::tui::layout::{Constraint, Direction, Margin, Rect};
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State, StateValue};
use crate::ui::state::FormState;
use crate::ui::theme::Theme;
use crate::ui::widget::WidgetComponent;
use crate::ui::widget::{Widget, WidgetComponent};
use super::container::Container;
use super::label::Label;
pub struct TextInput {
input: Widget<Container>,
placeholder: Widget<Label>,
show_placeholder: bool,
}
impl TextInput {
pub const PROP_MULTILINE: &str = "multiline";
pub const CMD_NEWLINE: &str = tui_realm_textarea::TEXTAREA_CMD_NEWLINE;
pub fn new(theme: Theme, title: &str) -> Self {
let input = TextArea::default()
.cursor_line_style(Style::reset())
.style(Style::default().fg(theme.colors.default_fg));
let container = super::container(&theme, Box::new(input));
Self {
input: container,
placeholder: super::label(title).foreground(theme.colors.input_placeholder_fg),
show_placeholder: true,
}
}
}
impl WidgetComponent for TextInput {
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
let focus = properties
.get_or(Attribute::Focus, AttrValue::Flag(false))
.unwrap_flag();
self.input.attr(Attribute::Focus, AttrValue::Flag(focus));
self.input.view(frame, area);
if self.show_placeholder {
let inner = area.inner(&Margin {
vertical: 1,
horizontal: 2,
});
self.placeholder.view(frame, inner);
}
}
fn state(&self) -> State {
self.input.state()
}
fn perform(&mut self, properties: &Props, cmd: Cmd) -> CmdResult {
let multiline = properties
.get_or(
Attribute::Custom(Self::PROP_MULTILINE),
AttrValue::Flag(false),
)
.unwrap_flag();
if !multiline && cmd == Cmd::Custom(Self::CMD_NEWLINE) {
CmdResult::None
} else {
let result = self.input.perform(cmd);
if let State::Vec(values) = self.input.state() {
if let Some(StateValue::String(input)) = values.first() {
self.show_placeholder = input.is_empty();
}
}
result
}
}
}
pub struct Form {
// This form's fields: title, tags, assignees, description.
inputs: Vec<Input>,
inputs: Vec<Widget<TextInput>>,
/// State that holds the current focus etc.
state: FormState,
}
impl Form {
pub fn new(_theme: Theme, inputs: Vec<Input>) -> Self {
pub fn new(_theme: Theme, inputs: Vec<Widget<TextInput>>) -> Self {
let state = FormState::new(Some(0), inputs.len());
Self { inputs, state }
@@ -70,13 +143,46 @@ impl WidgetComponent for Form {
self.state.focus_next();
CmdResult::None
}
Cmd::Submit => {
// Fold each input's vector of lines into a single string
// that containes newlines and return a state vector with
// each entry being the folded input string.
let states = self
.inputs
.iter()
.map(|input| {
if let State::Vec(values) = input.state() {
let mut text = String::new();
let lines = values
.iter()
.map(|value| match value {
StateValue::String(line) => line.clone(),
_ => String::new(),
})
.collect::<Vec<_>>();
let mut lines = lines.iter().peekable();
while let Some(line) = lines.next() {
text.push_str(line);
if lines.peek().is_some() {
text.push('\n');
}
}
StateValue::String(text)
} else {
StateValue::None
}
})
.collect::<Vec<_>>();
CmdResult::Submit(State::Vec(states))
}
_ => {
let focus = self.state.focus().unwrap_or(0);
if let Some(input) = self.inputs.get_mut(focus) {
input.perform(cmd)
} else {
CmdResult::None
return input.perform(cmd);
}
CmdResult::None
}
}
}
+14 -32
View File
@@ -3,10 +3,6 @@ use radicle::cob::thread::CommentId;
use radicle::cob::issue::Issue;
use radicle::cob::issue::IssueId;
use tui_realm_stdlib::Input;
use tuirealm::props::BorderType;
use tuirealm::props::Borders;
use tuirealm::props::Style;
use tuirealm::tui::layout::Constraint;
use tuirealm::tui::layout::Direction;
use tuirealm::tui::layout::Layout;
@@ -23,6 +19,7 @@ use crate::ui::cob;
use crate::ui::cob::IssueItem;
use crate::ui::context::Context;
use crate::ui::theme::Theme;
use crate::ui::widget::common::form::TextInput;
use super::*;
@@ -258,38 +255,17 @@ impl NewForm {
pub fn new(theme: &Theme) -> Self {
use tuirealm::props::Layout;
let foreground = theme.colors.default_fg;
let placeholder_style = Style::default().fg(theme.colors.input_placeholder_fg);
let inactive_style = Style::default().fg(theme.colors.container_border_fg);
let borders = Borders::default()
.modifiers(BorderType::Rounded)
.color(theme.colors.container_border_focus_fg);
let title = Input::default()
.foreground(foreground)
.borders(borders.clone())
.inactive(inactive_style)
.placeholder("Title", placeholder_style);
let tags = Input::default()
.foreground(foreground)
.borders(borders.clone())
.inactive(inactive_style)
.placeholder("Tags", placeholder_style);
let assignees = Input::default()
.foreground(foreground)
.borders(borders.clone())
.inactive(inactive_style)
.placeholder("Assignees", placeholder_style);
let description = Input::default()
.foreground(foreground)
.borders(borders)
.inactive(inactive_style)
.placeholder("Description", placeholder_style);
let title = Widget::new(TextInput::new(theme.clone(), "Title"));
let tags = Widget::new(TextInput::new(theme.clone(), "Tags"));
let assignees = Widget::new(TextInput::new(theme.clone(), "Assignees"));
let description = Widget::new(TextInput::new(theme.clone(), "Description"))
.custom(TextInput::PROP_MULTILINE, AttrValue::Flag(true));
let mut form = Widget::new(Form::new(
theme.clone(),
vec![title, tags, assignees, description],
));
form.attr(
Attribute::Layout,
AttrValue::Layout(
@@ -322,7 +298,13 @@ impl WidgetComponent for NewForm {
}
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
self.form.perform(cmd)
match self.form.perform(cmd) {
CmdResult::Submit(State::Vec(values)) => {
println!("{:?}", values);
CmdResult::None
}
_ => CmdResult::None,
}
}
}