tui: Restructure component modules
Signed-off-by: Erik Kundt <erik@zirkular.io>
This commit is contained in:
parent
0d98b84861
commit
708e202dcc
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue