tui: Add basic framework
Signed-off-by: Erik Kundt <erik@zirkular.io>
This commit is contained in:
parent
5ccf38079c
commit
62867d26bd
|
|
@ -29,6 +29,26 @@ dependencies = [
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.8",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "0.7.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "amplify"
|
name = "amplify"
|
||||||
version = "4.0.0-beta.17"
|
version = "4.0.0-beta.17"
|
||||||
|
|
@ -299,6 +319,12 @@ version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
|
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cassowary"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.78"
|
version = "1.0.78"
|
||||||
|
|
@ -987,6 +1013,9 @@ name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "headers"
|
name = "headers"
|
||||||
|
|
@ -1261,6 +1290,29 @@ dependencies = [
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy-regex"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a505da2f89befd87ab425d252795f0f285e100b43e7d22d29528df3d9a576793"
|
||||||
|
dependencies = [
|
||||||
|
"lazy-regex-proc_macros",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy-regex-proc_macros"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8edfc11b8f56ce85e207e62ea21557cfa09bb24a8f6b04ae181b086ff8611c22"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
|
@ -2075,6 +2127,17 @@ dependencies = [
|
||||||
"radicle-cli",
|
"radicle-cli",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "radicle-tui"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"lexopt",
|
||||||
|
"radicle-term",
|
||||||
|
"tui-realm-stdlib",
|
||||||
|
"tuirealm",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
|
@ -2170,6 +2233,8 @@ version = "1.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
|
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
"regex-syntax",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2458,6 +2523,12 @@ version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smawk"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "snapbox"
|
name = "snapbox"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
|
|
@ -2668,6 +2739,17 @@ dependencies = [
|
||||||
"redox_termios",
|
"redox_termios",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "textwrap"
|
||||||
|
version = "0.15.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d"
|
||||||
|
dependencies = [
|
||||||
|
"smawk",
|
||||||
|
"unicode-linebreak",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.38"
|
version = "1.0.38"
|
||||||
|
|
@ -2928,6 +3010,55 @@ version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tui"
|
||||||
|
version = "0.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cassowary",
|
||||||
|
"termion 1.5.6",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tui-realm-stdlib"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66f252bf8b07c6fd708ddd6349b5f044ae5b488b26929c745728d9c7e2cebfa6"
|
||||||
|
dependencies = [
|
||||||
|
"textwrap",
|
||||||
|
"tuirealm",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tuirealm"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "265411b5606f400459af94fbc5aae6a7bc0e98094d08cb5868390c932be88e26"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"lazy-regex",
|
||||||
|
"termion 1.5.6",
|
||||||
|
"thiserror",
|
||||||
|
"tui",
|
||||||
|
"tuirealm_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tuirealm_derive"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0adcdaf59881626555558eae08f8a53003c8a1961723b4d7a10c51599abbc81"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
|
|
@ -2946,6 +3077,16 @@ version = "1.0.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
|
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-linebreak"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-normalization"
|
name = "unicode-normalization"
|
||||||
version = "0.1.22"
|
version = "0.1.22"
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ members = [
|
||||||
"radicle-remote-helper",
|
"radicle-remote-helper",
|
||||||
"radicle-ssh",
|
"radicle-ssh",
|
||||||
"radicle-tools",
|
"radicle-tools",
|
||||||
|
"radicle-tui",
|
||||||
]
|
]
|
||||||
default-members = [
|
default-members = [
|
||||||
"radicle",
|
"radicle",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "radicle-tui"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Erik Kundt <erik@zirkular.io>"]
|
||||||
|
edition = "2021"
|
||||||
|
build = "../build.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "radicle-tui"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { version = "1" }
|
||||||
|
lexopt = { version = "0.2" }
|
||||||
|
tuirealm = { version = "1.8.0", default-features = false, features = [ "with-termion" ] }
|
||||||
|
tui-realm-stdlib = { version = "1.2.0", default-features = false, features = [ "with-termion" ] }
|
||||||
|
|
||||||
|
[dependencies.radicle-term]
|
||||||
|
version = "0"
|
||||||
|
path = "../radicle-term"
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
use std::hash::Hash;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use tuirealm::terminal::TerminalBridge;
|
||||||
|
use tuirealm::Frame;
|
||||||
|
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
|
||||||
|
|
||||||
|
mod ui;
|
||||||
|
|
||||||
|
/// Trait that must be implemented by client applications in order to be run
|
||||||
|
/// as tui-application using tui-realm. Implementors act as models to the
|
||||||
|
/// tui-realm application that can be polled for new messages, updated
|
||||||
|
/// accordingly and rendered with new state.
|
||||||
|
///
|
||||||
|
/// Please see `examples/` for further information on how to use it.
|
||||||
|
pub trait Tui<Id, Message>
|
||||||
|
where
|
||||||
|
Id: Eq + PartialEq + Clone + Hash,
|
||||||
|
Message: Eq,
|
||||||
|
{
|
||||||
|
/// Should initialize an application by mounting and activating components.
|
||||||
|
fn init(&mut self, app: &mut Application<Id, Message, NoUserEvent>) -> Result<()>;
|
||||||
|
|
||||||
|
/// Should update the current state by handling a message from the view.
|
||||||
|
fn update(&mut self, app: &mut Application<Id, Message, NoUserEvent>, interval: u64);
|
||||||
|
|
||||||
|
/// Should draw the application to a frame.
|
||||||
|
fn view(&mut self, app: &mut Application<Id, Message, NoUserEvent>, frame: &mut Frame);
|
||||||
|
|
||||||
|
/// Should return true if the application is requested to quit.
|
||||||
|
fn quit(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tui-window using the cross-platform Terminal helper provided
|
||||||
|
/// by tui-realm.
|
||||||
|
pub struct Window {
|
||||||
|
/// Helper around `Terminal` to quickly setup and perform on terminal.
|
||||||
|
pub terminal: TerminalBridge,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Window {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides a way to create and run a new tui-application.
|
||||||
|
impl Window {
|
||||||
|
/// Creates a tui-window using the default cross-platform Terminal
|
||||||
|
/// helper and panics if its creation fails.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
terminal: TerminalBridge::new().expect("Cannot create terminal bridge"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs this tui-window with the tui-application given and performs the
|
||||||
|
/// following steps:
|
||||||
|
/// 1. Enter alternative terminal screen
|
||||||
|
/// 2. Run main loop until application should quit and with each iteration
|
||||||
|
/// - poll new events (tick or user event)
|
||||||
|
/// - update application state
|
||||||
|
/// - redraw view
|
||||||
|
/// 3. Leave alternative terminal screen
|
||||||
|
pub fn run<T, Id, Message>(&mut self, tui: &mut T, interval: u64) -> Result<()>
|
||||||
|
where
|
||||||
|
T: Tui<Id, Message>,
|
||||||
|
Id: Eq + PartialEq + Clone + Hash,
|
||||||
|
Message: Eq,
|
||||||
|
{
|
||||||
|
let mut app = Application::init(
|
||||||
|
EventListenerCfg::default().default_input_listener(Duration::from_millis(interval)),
|
||||||
|
);
|
||||||
|
tui.init(&mut app)?;
|
||||||
|
|
||||||
|
while !tui.quit() {
|
||||||
|
tui.update(&mut app, interval);
|
||||||
|
|
||||||
|
self.terminal.raw_mut().draw(|frame| {
|
||||||
|
tui.view(&mut app, frame);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
use radicle_term as term;
|
||||||
|
|
||||||
|
pub const NAME: &str = "radicle-tui";
|
||||||
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
pub const GIT_HEAD: &str = env!("GIT_HEAD");
|
||||||
|
pub const FPS: u64 = 60;
|
||||||
|
|
||||||
|
pub const HELP: &str = r#"
|
||||||
|
Usage
|
||||||
|
|
||||||
|
radicle-tui [<option>...]
|
||||||
|
|
||||||
|
Options
|
||||||
|
|
||||||
|
--version Print version
|
||||||
|
--help Print help
|
||||||
|
|
||||||
|
"#;
|
||||||
|
|
||||||
|
struct Options;
|
||||||
|
|
||||||
|
impl Options {
|
||||||
|
fn from_env() -> Result<Self, anyhow::Error> {
|
||||||
|
use lexopt::prelude::*;
|
||||||
|
|
||||||
|
let mut parser = lexopt::Parser::from_env();
|
||||||
|
|
||||||
|
while let Some(arg) = parser.next()? {
|
||||||
|
match arg {
|
||||||
|
Long("version") => {
|
||||||
|
println!("{NAME} {VERSION}+{GIT_HEAD}");
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
Long("help") => {
|
||||||
|
println!("{HELP}");
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
_ => anyhow::bail!(arg.unexpected()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute() -> anyhow::Result<()> {
|
||||||
|
let _ = Options::from_env()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(err) = execute() {
|
||||||
|
term::error(format!("Error: rad-tui: {err}"));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
pub mod components;
|
||||||
|
pub mod layout;
|
||||||
|
pub mod theme;
|
||||||
|
pub mod widget;
|
||||||
|
|
||||||
|
use tuirealm::props::Attribute;
|
||||||
|
use tuirealm::{MockComponent, StateValue};
|
||||||
|
|
||||||
|
use components::{GlobalListener, Label, Shortcut, ShortcutBar};
|
||||||
|
use widget::Widget;
|
||||||
|
|
||||||
|
pub fn label(content: &str) -> Widget<Label> {
|
||||||
|
// TODO: Remove when size constraints are implemented
|
||||||
|
let width = content.chars().count() as u16;
|
||||||
|
let label = Label::new(StateValue::String(content.to_owned()));
|
||||||
|
|
||||||
|
Widget::new(label).height(1).width(width)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shortcut(theme: &theme::Theme, short: &str, long: &str) -> Widget<Shortcut> {
|
||||||
|
let short = label(short).foreground(theme.colors.shortcut_short_fg);
|
||||||
|
let divider = label(&theme.icons.whitespace.to_string());
|
||||||
|
let long = label(long).foreground(theme.colors.shortcut_long_fg);
|
||||||
|
|
||||||
|
// TODO: Remove when size constraints are implemented
|
||||||
|
let short_w = short.query(Attribute::Width).unwrap().unwrap_size();
|
||||||
|
let divider_w = divider.query(Attribute::Width).unwrap().unwrap_size();
|
||||||
|
let long_w = long.query(Attribute::Width).unwrap().unwrap_size();
|
||||||
|
let width = short_w.saturating_add(divider_w).saturating_add(long_w);
|
||||||
|
|
||||||
|
let shortcut = Shortcut::new(short, divider, long);
|
||||||
|
|
||||||
|
Widget::new(shortcut).height(1).width(width)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shortcut_bar(theme: &theme::Theme, shortcuts: Vec<Widget<Shortcut>>) -> Widget<ShortcutBar> {
|
||||||
|
let divider = label(&format!(" {} ", theme.icons.shortcutbar_divider))
|
||||||
|
.foreground(theme.colors.shortcutbar_divider_fg);
|
||||||
|
let shortcut_bar = ShortcutBar::new(shortcuts, divider);
|
||||||
|
|
||||||
|
Widget::new(shortcut_bar).height(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn global_listener() -> Widget<GlobalListener> {
|
||||||
|
Widget::new(GlobalListener::default())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
use tuirealm::command::{Cmd, CmdResult};
|
||||||
|
use tuirealm::props::{AttrValue, Attribute, Color, Props};
|
||||||
|
use tuirealm::tui::layout::Rect;
|
||||||
|
use tuirealm::{Frame, MockComponent, State, StateValue};
|
||||||
|
|
||||||
|
use super::layout;
|
||||||
|
use super::widget::{Widget, WidgetComponent};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct GlobalListener {}
|
||||||
|
|
||||||
|
impl WidgetComponent for GlobalListener {
|
||||||
|
fn view(&mut self, _properties: &Props, _frame: &mut Frame, _area: Rect) {}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
State::None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _cmd: Cmd) -> CmdResult {
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A label that can be styled using a foreground color and text modifiers.
|
||||||
|
/// Its height is fixed, its width depends on the length of the text it displays.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Label {
|
||||||
|
content: StateValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Label {
|
||||||
|
pub fn new(content: StateValue) -> Self {
|
||||||
|
Self { content }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetComponent for Label {
|
||||||
|
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
||||||
|
use tui_realm_stdlib::Label;
|
||||||
|
|
||||||
|
let display = properties
|
||||||
|
.get_or(Attribute::Display, AttrValue::Flag(true))
|
||||||
|
.unwrap_flag();
|
||||||
|
let foreground = properties
|
||||||
|
.get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
|
||||||
|
.unwrap_color();
|
||||||
|
|
||||||
|
if display {
|
||||||
|
let mut label = match properties.get(Attribute::TextProps) {
|
||||||
|
Some(modifiers) => Label::default()
|
||||||
|
.foreground(foreground)
|
||||||
|
.modifiers(modifiers.unwrap_text_modifiers())
|
||||||
|
.text(self.content.clone().unwrap_string()),
|
||||||
|
None => Label::default()
|
||||||
|
.foreground(foreground)
|
||||||
|
.text(self.content.clone().unwrap_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
label.view(frame, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
State::One(self.content.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _cmd: Cmd) -> CmdResult {
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A shortcut that consists of a label displaying the "hotkey", a label that displays
|
||||||
|
/// the action and a spacer between them.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Shortcut {
|
||||||
|
short: Widget<Label>,
|
||||||
|
divider: Widget<Label>,
|
||||||
|
long: Widget<Label>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shortcut {
|
||||||
|
pub fn new(short: Widget<Label>, divider: Widget<Label>, long: Widget<Label>) -> Self {
|
||||||
|
Self {
|
||||||
|
short,
|
||||||
|
divider,
|
||||||
|
long,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetComponent for Shortcut {
|
||||||
|
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
||||||
|
let display = properties
|
||||||
|
.get_or(Attribute::Display, AttrValue::Flag(true))
|
||||||
|
.unwrap_flag();
|
||||||
|
|
||||||
|
if display {
|
||||||
|
let labels: Vec<Box<dyn MockComponent>> = vec![
|
||||||
|
self.short.clone().to_boxed(),
|
||||||
|
self.divider.clone().to_boxed(),
|
||||||
|
self.long.clone().to_boxed(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let layout = layout::h_stack(labels, area);
|
||||||
|
for (mut shortcut, area) in layout {
|
||||||
|
shortcut.view(frame, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
State::None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _cmd: Cmd) -> CmdResult {
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A shortcut bar that displays multiple shortcuts and separates them with a
|
||||||
|
/// divider.
|
||||||
|
pub struct ShortcutBar {
|
||||||
|
shortcuts: Vec<Widget<Shortcut>>,
|
||||||
|
divider: Widget<Label>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShortcutBar {
|
||||||
|
pub fn new(shortcuts: Vec<Widget<Shortcut>>, divider: Widget<Label>) -> Self {
|
||||||
|
Self { shortcuts, divider }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetComponent for ShortcutBar {
|
||||||
|
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
||||||
|
let display = properties
|
||||||
|
.get_or(Attribute::Display, AttrValue::Flag(true))
|
||||||
|
.unwrap_flag();
|
||||||
|
|
||||||
|
if display {
|
||||||
|
let mut widgets: Vec<Box<dyn MockComponent>> = vec![];
|
||||||
|
let mut shortcuts = self.shortcuts.iter_mut().peekable();
|
||||||
|
|
||||||
|
while let Some(shortcut) = shortcuts.next() {
|
||||||
|
if shortcuts.peek().is_some() {
|
||||||
|
widgets.push(shortcut.clone().to_boxed());
|
||||||
|
widgets.push(self.divider.clone().to_boxed())
|
||||||
|
} else {
|
||||||
|
widgets.push(shortcut.clone().to_boxed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout = layout::h_stack(widgets, area);
|
||||||
|
for (mut widget, area) in layout {
|
||||||
|
widget.view(frame, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
State::None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _cmd: Cmd) -> CmdResult {
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
use tuirealm::props::{AttrValue, Attribute};
|
||||||
|
use tuirealm::tui::layout::Rect;
|
||||||
|
|
||||||
|
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
||||||
|
use tuirealm::MockComponent;
|
||||||
|
|
||||||
|
pub fn v_stack(
|
||||||
|
widgets: Vec<Box<dyn MockComponent>>,
|
||||||
|
area: Rect,
|
||||||
|
) -> Vec<(Box<dyn MockComponent>, Rect)> {
|
||||||
|
let constraints = widgets
|
||||||
|
.iter()
|
||||||
|
.map(|w| {
|
||||||
|
Constraint::Length(
|
||||||
|
w.query(Attribute::Height)
|
||||||
|
.unwrap_or(AttrValue::Size(0))
|
||||||
|
.unwrap_size(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(constraints)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
widgets.into_iter().zip(layout.into_iter()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn h_stack(
|
||||||
|
widgets: Vec<Box<dyn MockComponent>>,
|
||||||
|
area: Rect,
|
||||||
|
) -> Vec<(Box<dyn MockComponent>, Rect)> {
|
||||||
|
let constraints = widgets
|
||||||
|
.iter()
|
||||||
|
.map(|w| {
|
||||||
|
Constraint::Length(
|
||||||
|
w.query(Attribute::Width)
|
||||||
|
.unwrap_or(AttrValue::Size(0))
|
||||||
|
.unwrap_size(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(constraints)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
widgets.into_iter().zip(layout.into_iter()).collect()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
use tuirealm::props::Color;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Colors {
|
||||||
|
pub shortcut_short_fg: Color,
|
||||||
|
pub shortcut_long_fg: Color,
|
||||||
|
pub shortcutbar_divider_fg: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Icons {
|
||||||
|
pub whitespace: char,
|
||||||
|
pub shortcutbar_divider: char,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Radicle TUI theme. Can be defined in a JSON config file. e.g.:
|
||||||
|
///
|
||||||
|
/// {
|
||||||
|
/// "name": "Radicle Dark",
|
||||||
|
/// "colors": {
|
||||||
|
/// "foreground": "#ffffff",
|
||||||
|
/// "highlightedBackground": "#000000",
|
||||||
|
/// },
|
||||||
|
/// "icons": {
|
||||||
|
/// "workspaces.divider": "|",
|
||||||
|
/// "shortcuts.divider: "∙",
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Theme {
|
||||||
|
pub name: String,
|
||||||
|
pub colors: Colors,
|
||||||
|
pub icons: Icons,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_dark() -> Theme {
|
||||||
|
Theme {
|
||||||
|
name: String::from("Radicle Dark"),
|
||||||
|
colors: Colors {
|
||||||
|
shortcut_short_fg: Color::Rgb(100, 100, 100),
|
||||||
|
shortcut_long_fg: Color::Rgb(70, 70, 70),
|
||||||
|
shortcutbar_divider_fg: Color::Rgb(70, 70, 70),
|
||||||
|
},
|
||||||
|
icons: Icons {
|
||||||
|
whitespace: ' ',
|
||||||
|
shortcutbar_divider: '∙',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
use tuirealm::command::{Cmd, CmdResult};
|
||||||
|
use tuirealm::props::{AttrValue, Attribute, Color, Props};
|
||||||
|
use tuirealm::tui::layout::Rect;
|
||||||
|
use tuirealm::{Frame, MockComponent, State};
|
||||||
|
|
||||||
|
pub type BoxedWidget<T> = Box<Widget<T>>;
|
||||||
|
|
||||||
|
pub trait WidgetComponent {
|
||||||
|
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect);
|
||||||
|
|
||||||
|
fn state(&self) -> State;
|
||||||
|
|
||||||
|
fn perform(&mut self, cmd: Cmd) -> CmdResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Widget<T: WidgetComponent> {
|
||||||
|
component: T,
|
||||||
|
properties: Props,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: WidgetComponent> Widget<T> {
|
||||||
|
pub fn new(component: T) -> Self {
|
||||||
|
Widget {
|
||||||
|
component,
|
||||||
|
properties: Props::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn foreground(mut self, fg: Color) -> Self {
|
||||||
|
self.attr(Attribute::Foreground, AttrValue::Color(fg));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn highlight(mut self, fg: Color) -> Self {
|
||||||
|
self.attr(Attribute::HighlightedColor, AttrValue::Color(fg));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn background(mut self, bg: Color) -> Self {
|
||||||
|
self.attr(Attribute::Background, AttrValue::Color(bg));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(mut self, h: u16) -> Self {
|
||||||
|
self.attr(Attribute::Height, AttrValue::Size(h));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(mut self, w: u16) -> Self {
|
||||||
|
self.attr(Attribute::Width, AttrValue::Size(w));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_boxed(self) -> Box<Self> {
|
||||||
|
Box::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: WidgetComponent> MockComponent for Widget<T> {
|
||||||
|
fn view(&mut self, frame: &mut Frame, area: Rect) {
|
||||||
|
self.component.view(&self.properties, frame, area)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query(&self, attr: Attribute) -> Option<AttrValue> {
|
||||||
|
self.properties.get(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attr(&mut self, attr: Attribute, value: AttrValue) {
|
||||||
|
self.properties.set(attr, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
self.component.state()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
||||||
|
self.component.perform(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue