tui: Restructure component modules

Signed-off-by: Erik Kundt <erik@zirkular.io>
This commit is contained in:
Erik Kundt 2023-03-21 12:00:19 +01:00
parent 0d98b84861
commit 708e202dcc
8 changed files with 629 additions and 585 deletions

View File

@ -13,9 +13,10 @@ use tuirealm::{
}; };
use radicle_tui::ui; use radicle_tui::ui;
use radicle_tui::ui::components::{ use radicle_tui::ui::components::container::{GlobalListener, LabeledContainer};
GlobalListener, LabeledContainer, PropertyList, ShortcutBar, Workspaces, use radicle_tui::ui::components::context::Shortcuts;
}; use radicle_tui::ui::components::list::PropertyList;
use radicle_tui::ui::components::workspace::Workspaces;
use radicle_tui::ui::theme; use radicle_tui::ui::theme;
use radicle_tui::ui::widget::Widget; use radicle_tui::ui::widget::Widget;
@ -100,7 +101,7 @@ impl Tui<ComponentId, Message> for App {
app.mount( app.mount(
ComponentId::Shortcuts, ComponentId::Shortcuts,
ui::shortcut_bar( ui::shortcuts(
&theme, &theme,
vec![ vec![
ui::shortcut(&theme, "tab", "section"), ui::shortcut(&theme, "tab", "section"),
@ -217,7 +218,7 @@ impl Component<Message, NoUserEvent> for Widget<PropertyList> {
} }
} }
impl Component<Message, NoUserEvent> for Widget<ShortcutBar> { impl Component<Message, NoUserEvent> for Widget<Shortcuts> {
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> { fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
None None
} }

View File

@ -1,16 +1,17 @@
pub mod components; pub mod components;
pub mod layout; pub mod layout;
mod state; pub mod state;
pub mod theme; pub mod theme;
pub mod widget; pub mod widget;
use tuirealm::props::Attribute; use tuirealm::props::Attribute;
use tuirealm::{MockComponent, StateValue}; use tuirealm::{MockComponent, StateValue};
use components::{ use components::container::{GlobalListener, LabeledContainer, Tabs};
ContainerHeader, GlobalListener, Label, LabeledContainer, Property, PropertyList, Shortcut, use components::context::{Shortcut, Shortcuts};
ShortcutBar, Tabs, Workspaces, WorkspacesHeader, use components::label::Label;
}; use components::list::{Property, PropertyList};
use components::workspace::Workspaces;
use widget::Widget; use widget::Widget;
pub fn global_listener() -> Widget<GlobalListener> { pub fn global_listener() -> Widget<GlobalListener> {
@ -33,9 +34,7 @@ pub fn labeled_container(
let title = label(&format!(" {title} ")) let title = label(&format!(" {title} "))
.foreground(theme.colors.default_fg) .foreground(theme.colors.default_fg)
.background(theme.colors.labeled_container_bg); .background(theme.colors.labeled_container_bg);
let spacer = label(""); let container = LabeledContainer::new(title, component);
let header = Widget::new(ContainerHeader::new(title, spacer));
let container = LabeledContainer::new(header, component);
Widget::new(container).background(theme.colors.labeled_container_bg) Widget::new(container).background(theme.colors.labeled_container_bg)
} }
@ -56,10 +55,10 @@ pub fn shortcut(theme: &theme::Theme, short: &str, long: &str) -> Widget<Shortcu
Widget::new(shortcut).height(1).width(width) Widget::new(shortcut).height(1).width(width)
} }
pub fn shortcut_bar(theme: &theme::Theme, shortcuts: Vec<Widget<Shortcut>>) -> Widget<ShortcutBar> { pub fn shortcuts(theme: &theme::Theme, shortcuts: Vec<Widget<Shortcut>>) -> Widget<Shortcuts> {
let divider = label(&format!(" {} ", theme.icons.shortcutbar_divider)) let divider = label(&format!(" {} ", theme.icons.shortcutbar_divider))
.foreground(theme.colors.shortcutbar_divider_fg); .foreground(theme.colors.shortcutbar_divider_fg);
let shortcut_bar = ShortcutBar::new(shortcuts, divider); let shortcut_bar = Shortcuts::new(shortcuts, divider);
Widget::new(shortcut_bar).height(1) Widget::new(shortcut_bar).height(1)
} }
@ -106,8 +105,7 @@ pub fn workspaces(
children: Vec<Box<dyn MockComponent>>, children: Vec<Box<dyn MockComponent>>,
) -> Widget<Workspaces> { ) -> Widget<Workspaces> {
let info = label(info).foreground(theme.colors.workspaces_info_fg); let info = label(info).foreground(theme.colors.workspaces_info_fg);
let header = Widget::new(WorkspacesHeader::new(tabs, info)); let workspaces = Workspaces::new(tabs, info, children);
let workspaces = Workspaces::new(header, children);
Widget::new(workspaces) Widget::new(workspaces)
} }

View File

@ -1,568 +1,5 @@
use tui_realm_stdlib::Phantom; pub mod container;
pub mod context;
use tuirealm::command::{Cmd, CmdResult}; pub mod label;
use tuirealm::props::{AttrValue, Attribute, Color, Props, Style}; pub mod list;
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect}; pub mod workspace;
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
/// the application). This component can be used in conjunction with SubEventClause
/// to handle those events.
#[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
}
}
/// Some user events need to be handled globally (e.g. user presses key `q` to quit
/// the application). This component can be used in conjunction with SubEventClause
/// to handle those events.
#[derive(Default, MockComponent)]
pub struct GlobalPhantom {
component: Phantom,
}
/// 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();
let background = properties
.get_or(Attribute::Background, AttrValue::Color(Color::Reset))
.unwrap_color();
if display {
let mut label = match properties.get(Attribute::TextProps) {
Some(modifiers) => Label::default()
.foreground(foreground)
.background(background)
.modifiers(modifiers.unwrap_text_modifiers())
.text(self.content.clone().unwrap_string()),
None => Label::default()
.foreground(foreground)
.background(background)
.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
}
}
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 {
content: Widget<Label>,
spacer: Widget<Label>,
}
impl ContainerHeader {
pub fn new(content: Widget<Label>, spacer: Widget<Label>) -> Self {
Self { content, spacer }
}
}
impl WidgetComponent for ContainerHeader {
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.content.clone().to_boxed(),
self.spacer.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
}
}
pub struct LabeledContainer {
header: Widget<ContainerHeader>,
component: Box<dyn MockComponent>,
}
impl LabeledContainer {
pub fn new(header: Widget<ContainerHeader>, component: Box<dyn MockComponent>) -> Self {
Self { header, component }
}
}
impl WidgetComponent for LabeledContainer {
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
let display = properties
.get_or(Attribute::Display, AttrValue::Flag(true))
.unwrap_flag();
let background = properties
.get_or(Attribute::Background, AttrValue::Color(Color::Reset))
.unwrap_color();
let header_height = self
.header
.query(Attribute::Height)
.unwrap_or(AttrValue::Size(1))
.unwrap_size();
if display {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(header_height), Constraint::Length(0)].as_ref())
.split(area);
self.header.view(frame, layout[0]);
// Make some space on the left
let inner_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Length(1), Constraint::Min(0)].as_ref())
.split(layout[1]);
// reverse draw order: child needs to be drawn first?
self.component.view(frame, inner_layout[1]);
let block = Block::default().style(Style::default().bg(background));
frame.render_widget(block, layout[1]);
}
}
fn state(&self) -> State {
State::None
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
self.component.perform(cmd)
}
}
/// 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
}
}
/// A component that displays a labeled property.
#[derive(Clone)]
pub struct Property {
label: Widget<Label>,
divider: Widget<Label>,
property: Widget<Label>,
}
impl Property {
pub fn new(label: Widget<Label>, divider: Widget<Label>, property: Widget<Label>) -> Self {
Self {
label,
divider,
property,
}
}
}
impl WidgetComponent for Property {
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.label.clone().to_boxed(),
self.divider.clone().to_boxed(),
self.property.clone().to_boxed(),
];
let layout = layout::h_stack(labels, area);
for (mut label, area) in layout {
label.view(frame, area);
}
}
}
fn state(&self) -> State {
State::None
}
fn perform(&mut self, _cmd: Cmd) -> CmdResult {
CmdResult::None
}
}
/// A component that can display lists of labeled properties
#[derive(Default)]
#[allow(clippy::vec_box)]
pub struct PropertyList {
properties: Vec<Widget<Property>>,
}
impl PropertyList {
pub fn new(properties: Vec<Widget<Property>>) -> Self {
Self { properties }
}
}
impl WidgetComponent for PropertyList {
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 properties = self
.properties
.iter()
.map(|property| property.clone().to_boxed() as Box<dyn MockComponent>)
.collect();
let layout = layout::v_stack(properties, area);
for (mut property, area) in layout {
property.view(frame, area);
}
}
}
fn state(&self) -> State {
State::None
}
fn perform(&mut self, _cmd: Cmd) -> CmdResult {
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,209 @@
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::text::{Span, Spans};
use tuirealm::tui::widgets::Block;
use tuirealm::{Frame, MockComponent, State, StateValue};
use crate::ui::components::label::Label;
use crate::ui::layout;
use crate::ui::state::TabState;
use crate::ui::widget::{Widget, WidgetComponent};
/// Some user events need to be handled globally (e.g. user presses key `q` to quit
/// the application). This component can be used in conjunction with SubEventClause
/// to handle those events.
#[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
}
}
/// Some user events need to be handled globally (e.g. user presses key `q` to quit
/// the application). This component can be used in conjunction with SubEventClause
/// to handle those events.
#[derive(Default, MockComponent)]
pub struct GlobalPhantom {
component: Phantom,
}
/// 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 = tuirealm::tui::widgets::Tabs::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,
}
}
}
/// A labeled container header.
#[derive(Clone)]
struct Header {
content: Widget<Label>,
}
impl Header {
pub fn new(content: Widget<Label>) -> Self {
Self { content }
}
}
impl WidgetComponent for Header {
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
let display = properties
.get_or(Attribute::Display, AttrValue::Flag(true))
.unwrap_flag();
let spacer = Widget::new(Label::new(StateValue::String(String::default()))).to_boxed();
if display {
let labels: Vec<Box<dyn MockComponent>> = vec![self.content.clone().to_boxed(), spacer];
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
}
}
pub struct LabeledContainer {
header: Widget<Header>,
component: Box<dyn MockComponent>,
}
impl LabeledContainer {
pub fn new(content: Widget<Label>, component: Box<dyn MockComponent>) -> Self {
Self {
header: Widget::new(Header::new(content)),
component,
}
}
}
impl WidgetComponent for LabeledContainer {
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
let display = properties
.get_or(Attribute::Display, AttrValue::Flag(true))
.unwrap_flag();
let background = properties
.get_or(Attribute::Background, AttrValue::Color(Color::Reset))
.unwrap_color();
let header_height = self
.header
.query(Attribute::Height)
.unwrap_or(AttrValue::Size(1))
.unwrap_size();
if display {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(header_height), Constraint::Length(0)].as_ref())
.split(area);
self.header.view(frame, layout[0]);
// Make some space on the left
let inner_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Length(1), Constraint::Min(0)].as_ref())
.split(layout[1]);
// reverse draw order: child needs to be drawn first?
self.component.view(frame, inner_layout[1]);
let block = Block::default().style(Style::default().bg(background));
frame.render_widget(block, layout[1]);
}
}
fn state(&self) -> State {
State::None
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
self.component.perform(cmd)
}
}

View File

@ -0,0 +1,104 @@
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{AttrValue, Attribute, Props};
use tuirealm::tui::layout::Rect;
use tuirealm::{Frame, MockComponent, State};
use crate::ui::components::label::Label;
use crate::ui::layout;
use crate::ui::widget::{Widget, WidgetComponent};
/// 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 Shortcuts {
shortcuts: Vec<Widget<Shortcut>>,
divider: Widget<Label>,
}
impl Shortcuts {
pub fn new(shortcuts: Vec<Widget<Shortcut>>, divider: Widget<Label>) -> Self {
Self { shortcuts, divider }
}
}
impl WidgetComponent for Shortcuts {
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
}
}

View File

@ -0,0 +1,66 @@
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{AttrValue, Attribute, Color, Props, Style};
use tuirealm::tui::layout::Rect;
use tuirealm::tui::text::Span;
use tuirealm::{Frame, MockComponent, State, StateValue};
use crate::ui::widget::{Widget, WidgetComponent};
/// 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();
let background = properties
.get_or(Attribute::Background, AttrValue::Color(Color::Reset))
.unwrap_color();
if display {
let mut label = match properties.get(Attribute::TextProps) {
Some(modifiers) => Label::default()
.foreground(foreground)
.background(background)
.modifiers(modifiers.unwrap_text_modifiers())
.text(self.content.clone().unwrap_string()),
None => Label::default()
.foreground(foreground)
.background(background)
.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
}
}
impl From<&Widget<Label>> for Span<'_> {
fn from(label: &Widget<Label>) -> Self {
Span::styled(label.content.clone().unwrap_string(), Style::default())
}
}

View File

@ -0,0 +1,97 @@
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{AttrValue, Attribute, Props};
use tuirealm::tui::layout::Rect;
use tuirealm::{Frame, MockComponent, State};
use crate::ui::components::label::Label;
use crate::ui::layout;
use crate::ui::widget::{Widget, WidgetComponent};
/// A component that displays a labeled property.
#[derive(Clone)]
pub struct Property {
label: Widget<Label>,
divider: Widget<Label>,
property: Widget<Label>,
}
impl Property {
pub fn new(label: Widget<Label>, divider: Widget<Label>, property: Widget<Label>) -> Self {
Self {
label,
divider,
property,
}
}
}
impl WidgetComponent for Property {
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.label.clone().to_boxed(),
self.divider.clone().to_boxed(),
self.property.clone().to_boxed(),
];
let layout = layout::h_stack(labels, area);
for (mut label, area) in layout {
label.view(frame, area);
}
}
}
fn state(&self) -> State {
State::None
}
fn perform(&mut self, _cmd: Cmd) -> CmdResult {
CmdResult::None
}
}
/// A component that can display lists of labeled properties
#[derive(Default)]
#[allow(clippy::vec_box)]
pub struct PropertyList {
properties: Vec<Widget<Property>>,
}
impl PropertyList {
pub fn new(properties: Vec<Widget<Property>>) -> Self {
Self { properties }
}
}
impl WidgetComponent for PropertyList {
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 properties = self
.properties
.iter()
.map(|property| property.clone().to_boxed() as Box<dyn MockComponent>)
.collect();
let layout = layout::v_stack(properties, area);
for (mut property, area) in layout {
property.view(frame, area);
}
}
}
fn state(&self) -> State {
State::None
}
fn perform(&mut self, _cmd: Cmd) -> CmdResult {
CmdResult::None
}
}

View File

@ -0,0 +1,132 @@
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{AttrValue, Attribute, Props};
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
use tuirealm::{Frame, MockComponent, State};
use crate::ui::components::label::Label;
use crate::ui::widget::{Widget, WidgetComponent};
use super::container::Tabs;
/// Workspace header that displays all labels horizontally aligned and separated
/// by a divider. Highlights the label defined by the current tab index.
#[derive(Clone)]
struct Header {
tabs: Widget<Tabs>,
info: Widget<Label>,
}
impl Header {
pub fn new(tabs: Widget<Tabs>, info: Widget<Label>) -> Self {
Self { tabs, info }
}
}
impl WidgetComponent for Header {
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<Header>,
children: Vec<Box<dyn MockComponent>>,
}
impl Workspaces {
pub fn new(
tabs: Widget<Tabs>,
info: Widget<Label>,
children: Vec<Box<dyn MockComponent>>,
) -> Self {
Self {
header: Widget::new(Header::new(tabs, info)),
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(),
)
}
}