tui: Implement workspaces container

Signed-off-by: Erik Kundt <erik@zirkular.io>
This commit is contained in:
Erik Kundt 2023-03-08 16:49:08 +01:00 committed by Alexis Sellier
parent 791c378d4e
commit d331389ada
No known key found for this signature in database
6 changed files with 311 additions and 17 deletions

View File

@ -2,14 +2,20 @@ use std::time::Duration;
use anyhow::Result;
use tui_realm_stdlib::Phantom;
use tuirealm::application::PollStrategy;
use tuirealm::command::{Cmd, Direction as MoveDirection};
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 tuirealm::{
Application, Component, Frame, MockComponent, NoUserEvent, Sub, SubClause, SubEventClause,
};
use radicle_tui::ui;
use radicle_tui::ui::components::{GlobalListener, LabeledContainer, PropertyList, ShortcutBar};
use radicle_tui::ui::components::{
GlobalListener, LabeledContainer, PropertyList, ShortcutBar, Workspaces,
};
use radicle_tui::ui::theme;
use radicle_tui::ui::widget::Widget;
@ -34,7 +40,7 @@ pub enum Message {
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum ComponentId {
Workspaces,
ShortcutBar,
Shortcuts,
GlobalListener,
}
@ -54,27 +60,46 @@ impl Tui<ComponentId, Message> for App {
fn init(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>) -> Result<()> {
let theme = theme::default_dark();
let dashboard = 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();
app.mount(
ComponentId::Workspaces,
ui::labeled_container(
ui::workspaces(
&theme,
"about",
ui::property_list(
self.project.name(),
ui::tabs(
&theme,
vec![
ui::property(&theme, "id", &self.id.to_string()),
ui::property(&theme, "name", self.project.name()),
ui::property(&theme, "description", self.project.description()),
ui::label("dashboard"),
ui::label("issues"),
ui::label("patches"),
],
)
.to_boxed(),
),
vec![
dashboard,
Box::<Phantom>::default(),
Box::<Phantom>::default(),
],
)
.to_boxed(),
vec![],
)?;
app.mount(
ComponentId::ShortcutBar,
ComponentId::Shortcuts,
ui::shortcut_bar(
&theme,
vec![
@ -100,7 +125,7 @@ impl Tui<ComponentId, Message> for App {
)?;
// We need to give focus to a component then
app.active(&ComponentId::ShortcutBar)?;
app.active(&ComponentId::Workspaces)?;
Ok(())
}
@ -113,7 +138,7 @@ impl Tui<ComponentId, Message> for App {
let area = frame.size();
let margin_h = 1u16;
let shortcuts_h = app
.query(&ComponentId::ShortcutBar, Attribute::Height)
.query(&ComponentId::Shortcuts, Attribute::Height)
.ok()
.flatten()
.unwrap_or(AttrValue::Size(0))
@ -135,7 +160,7 @@ impl Tui<ComponentId, Message> for App {
.split(area);
app.view(&ComponentId::Workspaces, frame, layout[0]);
app.view(&ComponentId::ShortcutBar, frame, layout[1]);
app.view(&ComponentId::Shortcuts, frame, layout[1]);
}
fn update(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>, interval: u64) {
@ -168,6 +193,18 @@ impl Component<Message, NoUserEvent> for Widget<GlobalListener> {
}
}
impl Component<Message, NoUserEvent> for Widget<Workspaces> {
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
match event {
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
self.perform(Cmd::Move(MoveDirection::Right));
None
}
_ => None,
}
}
}
impl Component<Message, NoUserEvent> for Widget<LabeledContainer> {
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
None

View File

@ -1,5 +1,6 @@
pub mod components;
pub mod layout;
mod state;
pub mod theme;
pub mod widget;
@ -8,7 +9,7 @@ use tuirealm::{MockComponent, StateValue};
use components::{
ContainerHeader, GlobalListener, Label, LabeledContainer, Property, PropertyList, Shortcut,
ShortcutBar,
ShortcutBar, Tabs, Workspaces, WorkspacesHeader,
};
use widget::Widget;
@ -87,3 +88,26 @@ pub fn property_list(
Widget::new(property_list)
}
pub fn tabs(theme: &theme::Theme, tabs: Vec<Widget<Label>>) -> Widget<Tabs> {
let divider = label(&theme.icons.tab_divider.to_string());
let tabs = Tabs::new(tabs, divider);
Widget::new(tabs)
.height(1)
.foreground(theme.colors.tabs_fg)
.highlight(theme.colors.tabs_highlighted_fg)
}
pub fn workspaces(
theme: &theme::Theme,
info: &str,
tabs: Widget<Tabs>,
children: Vec<Box<dyn MockComponent>>,
) -> Widget<Workspaces> {
let info = label(info).foreground(theme.colors.workspaces_info_fg);
let header = Widget::new(WorkspacesHeader::new(tabs, info));
let workspaces = Workspaces::new(header, children);
Widget::new(workspaces)
}

View File

@ -3,10 +3,12 @@ use tui_realm_stdlib::Phantom;
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{AttrValue, Attribute, Color, Props, Style};
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
use tuirealm::tui::widgets::Block;
use tuirealm::tui::text::{Span, Spans};
use tuirealm::tui::widgets::{Block, Tabs as TuiTabs};
use tuirealm::{Frame, MockComponent, State, StateValue};
use super::layout;
use super::state::TabState;
use super::widget::{Widget, WidgetComponent};
/// Some user events need to be handled globally (e.g. user presses key `q` to quit
@ -88,6 +90,12 @@ impl WidgetComponent for Label {
}
}
impl From<&Widget<Label>> for Span<'_> {
fn from(label: &Widget<Label>) -> Self {
Span::styled(label.content.clone().unwrap_string(), Style::default())
}
}
/// A labeled container header.
#[derive(Clone)]
pub struct ContainerHeader {
@ -368,3 +376,193 @@ impl WidgetComponent for PropertyList {
CmdResult::None
}
}
/// A tab header that displays all labels horizontally aligned and separated
/// by a divider. Highlights the label defined by the current tab index.
#[derive(Clone)]
pub struct Tabs {
tabs: Vec<Widget<Label>>,
divider: Widget<Label>,
state: TabState,
}
impl Tabs {
pub fn new(tabs: Vec<Widget<Label>>, divider: Widget<Label>) -> Self {
Self {
tabs,
divider,
state: TabState {
selected: 0,
len: 3,
},
}
}
}
impl WidgetComponent for Tabs {
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
let selected = self.state().unwrap_one().unwrap_u16();
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();
let highlight = properties
.get_or(Attribute::HighlightedColor, AttrValue::Color(Color::Reset))
.unwrap_color();
if display {
let spans = self
.tabs
.iter()
.map(|tab| Spans::from(vec![Span::from(tab)]))
.collect::<Vec<_>>();
let tabs = TuiTabs::new(spans)
.style(Style::default().fg(foreground))
.highlight_style(Style::default().fg(highlight))
.divider(Span::from(&self.divider))
.select(selected as usize);
frame.render_widget(tabs, area);
}
}
fn state(&self) -> State {
State::One(StateValue::U16(self.state.selected))
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
use tuirealm::command::Direction;
match cmd {
Cmd::Move(Direction::Right) => {
let prev = self.state.selected;
self.state.incr_tab_index(true);
if prev != self.state.selected {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
_ => CmdResult::None,
}
}
}
/// Workspace header that displays all labels horizontally aligned and separated
/// by a divider. Highlights the label defined by the current tab index.
#[derive(Clone)]
pub struct WorkspacesHeader {
tabs: Widget<Tabs>,
info: Widget<Label>,
}
impl WorkspacesHeader {
pub fn new(tabs: Widget<Tabs>, info: Widget<Label>) -> Self {
Self { tabs, info }
}
}
impl WidgetComponent for WorkspacesHeader {
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
let display = properties
.get_or(Attribute::Display, AttrValue::Flag(true))
.unwrap_flag();
let info_width = self
.info
.query(Attribute::Width)
.unwrap_or(AttrValue::Size(1))
.unwrap_size();
let tabs_width = area.width.saturating_sub(info_width);
if display {
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Length(tabs_width),
Constraint::Length(info_width),
]
.as_ref(),
)
.split(area);
self.tabs.view(frame, layout[0]);
self.info.view(frame, layout[1]);
}
}
fn state(&self) -> State {
State::None
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
self.tabs.perform(cmd)
}
}
/// A container with a tab header. Displays the component selected by the index
/// held in the header state.
pub struct Workspaces {
header: Widget<WorkspacesHeader>,
children: Vec<Box<dyn MockComponent>>,
}
impl Workspaces {
pub fn new(header: Widget<WorkspacesHeader>, children: Vec<Box<dyn MockComponent>>) -> Self {
Self { header, children }
}
}
impl WidgetComponent for Workspaces {
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
let display = properties
.get_or(Attribute::Display, AttrValue::Flag(true))
.unwrap_flag();
let header_height = self
.header
.query(Attribute::Height)
.unwrap_or(AttrValue::Size(1))
.unwrap_size();
let selected = self.header.tabs.state().unwrap_one().unwrap_u16();
if display {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(header_height),
Constraint::Length(1),
Constraint::Length(0),
]
.as_ref(),
)
.split(area);
self.header.view(frame, layout[0]);
if let Some(child) = self.children.get_mut(selected as usize) {
child.view(frame, layout[2]);
}
}
}
fn state(&self) -> State {
State::None
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
CmdResult::Batch(
[
self.children
.iter_mut()
.map(|child| child.perform(cmd))
.collect(),
vec![self.header.perform(cmd)],
]
.concat(),
)
}
}

View File

@ -0,0 +1,17 @@
/// State that holds the index of a selected tab item and the count of all tab items.
/// The index can be increased and will start at 0, if length was reached.
#[derive(Clone, Default)]
pub struct TabState {
pub selected: u16,
pub len: u16,
}
impl TabState {
pub fn incr_tab_index(&mut self, rewind: bool) {
if self.selected + 1 < self.len {
self.selected += 1;
} else if rewind {
self.selected = 0;
}
}
}

View File

@ -3,6 +3,9 @@ use tuirealm::props::Color;
#[derive(Debug)]
pub struct Colors {
pub default_fg: Color,
pub tabs_fg: Color,
pub tabs_highlighted_fg: Color,
pub workspaces_info_fg: Color,
pub labeled_container_bg: Color,
pub property_name_fg: Color,
pub property_divider_fg: Color,
@ -15,6 +18,7 @@ pub struct Colors {
pub struct Icons {
pub property_divider: char,
pub shortcutbar_divider: char,
pub tab_divider: char,
pub whitespace: char,
}
@ -44,6 +48,9 @@ pub fn default_dark() -> Theme {
name: String::from("Radicle Dark"),
colors: Colors {
default_fg: Color::Rgb(200, 200, 200),
tabs_fg: Color::Rgb(100, 100, 100),
tabs_highlighted_fg: Color::Rgb(38, 162, 105),
workspaces_info_fg: Color::Rgb(220, 140, 40),
labeled_container_bg: Color::Rgb(20, 20, 20),
property_name_fg: Color::Rgb(85, 85, 255),
property_divider_fg: Color::Rgb(10, 206, 209),
@ -54,6 +61,7 @@ pub fn default_dark() -> Theme {
icons: Icons {
property_divider: '',
shortcutbar_divider: '',
tab_divider: '|',
whitespace: ' ',
},
}

View File

@ -1,3 +1,5 @@
use std::ops::Deref;
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{AttrValue, Attribute, Color, Props};
use tuirealm::tui::layout::Rect;
@ -19,6 +21,14 @@ pub struct Widget<T: WidgetComponent> {
properties: Props,
}
impl<T: WidgetComponent> Deref for Widget<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.component
}
}
impl<T: WidgetComponent> Widget<T> {
pub fn new(component: T) -> Self {
Widget {