radicle-heartwood-lfs/radicle-tui/src/app.rs

188 lines
5.2 KiB
Rust

use std::time::Duration;
use anyhow::Result;
use tuirealm::application::PollStrategy;
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers};
use tuirealm::props::{AttrValue, Attribute};
use tuirealm::tui::layout::{Constraint, Direction, Layout};
use tuirealm::{Application, Component, Frame, NoUserEvent, Sub, SubClause, SubEventClause};
use radicle_tui::ui;
use radicle_tui::ui::components::{GlobalListener, LabeledContainer, PropertyList, ShortcutBar};
use radicle_tui::ui::theme;
use radicle_tui::ui::widget::Widget;
use radicle_tui::Tui;
use radicle::identity::{Id, Project};
#[allow(dead_code)]
pub struct App {
id: Id,
project: Project,
quit: bool,
}
/// Messages handled by this application.
#[derive(Debug, Eq, PartialEq)]
pub enum Message {
Quit,
}
/// All components known to this application.
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum ComponentId {
Workspaces,
ShortcutBar,
GlobalListener,
}
/// Creates a new application using a tui-realm-application, mounts all
/// components and sets focus to a default one.
impl App {
pub fn new(id: Id, project: Project) -> Self {
Self {
id,
project,
quit: false,
}
}
}
impl Tui<ComponentId, Message> for App {
fn init(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>) -> Result<()> {
let theme = theme::default_dark();
app.mount(
ComponentId::Workspaces,
ui::labeled_container(
&theme,
"about",
ui::property_list(
&theme,
vec![
ui::property(&theme, "id", &self.id.to_string()),
ui::property(&theme, "name", self.project.name()),
ui::property(&theme, "description", self.project.description()),
],
)
.to_boxed(),
)
.to_boxed(),
vec![],
)?;
app.mount(
ComponentId::ShortcutBar,
ui::shortcut_bar(
&theme,
vec![
ui::shortcut(&theme, "tab", "section"),
ui::shortcut(&theme, "q", "quit"),
],
)
.to_boxed(),
vec![],
)?;
// Add global key listener and subscribe to key events
app.mount(
ComponentId::GlobalListener,
ui::global_listener().to_boxed(),
vec![Sub::new(
SubEventClause::Keyboard(KeyEvent {
code: Key::Char('q'),
modifiers: KeyModifiers::NONE,
}),
SubClause::Always,
)],
)?;
// We need to give focus to a component then
app.active(&ComponentId::ShortcutBar)?;
Ok(())
}
fn view(
&mut self,
app: &mut Application<ComponentId, Message, NoUserEvent>,
frame: &mut Frame,
) {
let area = frame.size();
let margin_h = 1u16;
let shortcuts_h = app
.query(&ComponentId::ShortcutBar, Attribute::Height)
.ok()
.flatten()
.unwrap_or(AttrValue::Size(0))
.unwrap_size();
let workspaces_h = area
.height
.saturating_sub(shortcuts_h.saturating_add(margin_h));
let layout = Layout::default()
.direction(Direction::Vertical)
.horizontal_margin(margin_h)
.constraints(
[
Constraint::Length(workspaces_h),
Constraint::Length(shortcuts_h),
]
.as_ref(),
)
.split(area);
app.view(&ComponentId::Workspaces, frame, layout[0]);
app.view(&ComponentId::ShortcutBar, frame, layout[1]);
}
fn update(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>, interval: u64) {
if let Ok(messages) = app.tick(PollStrategy::TryFor(Duration::from_millis(interval))) {
for message in messages {
match message {
Message::Quit => self.quit = true,
}
}
}
}
fn quit(&self) -> bool {
self.quit
}
}
/// Since the framework does not know the type of messages that are being
/// passed around in the app, the following handlers need to be implemented for
/// each component used.
impl Component<Message, NoUserEvent> for Widget<GlobalListener> {
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
match event {
Event::Keyboard(KeyEvent {
code: Key::Char('q'),
..
}) => Some(Message::Quit),
_ => None,
}
}
}
impl Component<Message, NoUserEvent> for Widget<LabeledContainer> {
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
None
}
}
impl Component<Message, NoUserEvent> for Widget<PropertyList> {
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
None
}
}
impl Component<Message, NoUserEvent> for Widget<ShortcutBar> {
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
None
}
}