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",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "amplify"
|
||||
version = "4.0.0-beta.17"
|
||||
|
|
@ -299,6 +319,12 @@ version = "1.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.78"
|
||||
|
|
@ -987,6 +1013,9 @@ name = "hashbrown"
|
|||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "headers"
|
||||
|
|
@ -1261,6 +1290,29 @@ dependencies = [
|
|||
"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]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
|
|
@ -2075,6 +2127,17 @@ dependencies = [
|
|||
"radicle-cli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "radicle-tui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lexopt",
|
||||
"radicle-term",
|
||||
"tui-realm-stdlib",
|
||||
"tuirealm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
|
|
@ -2170,6 +2233,8 @@ version = "1.7.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
|
|
@ -2458,6 +2523,12 @@ version = "1.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
|
||||
|
||||
[[package]]
|
||||
name = "snapbox"
|
||||
version = "0.4.4"
|
||||
|
|
@ -2668,6 +2739,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "thiserror"
|
||||
version = "1.0.38"
|
||||
|
|
@ -2928,6 +3010,55 @@ version = "0.2.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "typenum"
|
||||
version = "1.16.0"
|
||||
|
|
@ -2946,6 +3077,16 @@ version = "1.0.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.22"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ members = [
|
|||
"radicle-remote-helper",
|
||||
"radicle-ssh",
|
||||
"radicle-tools",
|
||||
"radicle-tui",
|
||||
]
|
||||
default-members = [
|
||||
"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