mirror of
https://seed.radicle.dev/rad:z39mP9rQAaGmERfUMPULfPUi473tY.git
synced 2026-11-11 22:20:45 +08:00
Implement form input w/ tui-realm-textarea
This commit is contained in:
Generated
+128
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user