tui: Implement workspaces container
Signed-off-by: Erik Kundt <erik@zirkular.io>
This commit is contained in:
parent
791c378d4e
commit
d331389ada
|
|
@ -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,9 +60,7 @@ 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(
|
||||
let dashboard = ui::labeled_container(
|
||||
&theme,
|
||||
"about",
|
||||
ui::property_list(
|
||||
|
|
@ -69,12 +73,33 @@ impl Tui<ComponentId, Message> for App {
|
|||
)
|
||||
.to_boxed(),
|
||||
)
|
||||
.to_boxed();
|
||||
|
||||
app.mount(
|
||||
ComponentId::Workspaces,
|
||||
ui::workspaces(
|
||||
&theme,
|
||||
self.project.name(),
|
||||
ui::tabs(
|
||||
&theme,
|
||||
vec![
|
||||
ui::label("dashboard"),
|
||||
ui::label("issues"),
|
||||
ui::label("patches"),
|
||||
],
|
||||
),
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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: ' ',
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue