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::components::{
|
||||
GlobalListener, LabeledContainer, PropertyList, ShortcutBar, Workspaces,
|
||||
};
|
||||
use radicle_tui::ui::components::container::{GlobalListener, LabeledContainer};
|
||||
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::widget::Widget;
|
||||
|
||||
|
|
@ -100,7 +101,7 @@ impl Tui<ComponentId, Message> for App {
|
|||
|
||||
app.mount(
|
||||
ComponentId::Shortcuts,
|
||||
ui::shortcut_bar(
|
||||
ui::shortcuts(
|
||||
&theme,
|
||||
vec![
|
||||
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> {
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
pub mod components;
|
||||
pub mod layout;
|
||||
mod state;
|
||||
pub mod state;
|
||||
pub mod theme;
|
||||
pub mod widget;
|
||||
|
||||
use tuirealm::props::Attribute;
|
||||
use tuirealm::{MockComponent, StateValue};
|
||||
|
||||
use components::{
|
||||
ContainerHeader, GlobalListener, Label, LabeledContainer, Property, PropertyList, Shortcut,
|
||||
ShortcutBar, Tabs, Workspaces, WorkspacesHeader,
|
||||
};
|
||||
use components::container::{GlobalListener, LabeledContainer, Tabs};
|
||||
use components::context::{Shortcut, Shortcuts};
|
||||
use components::label::Label;
|
||||
use components::list::{Property, PropertyList};
|
||||
use components::workspace::Workspaces;
|
||||
use widget::Widget;
|
||||
|
||||
pub fn global_listener() -> Widget<GlobalListener> {
|
||||
|
|
@ -33,9 +34,7 @@ pub fn labeled_container(
|
|||
let title = label(&format!(" {title} "))
|
||||
.foreground(theme.colors.default_fg)
|
||||
.background(theme.colors.labeled_container_bg);
|
||||
let spacer = label("");
|
||||
let header = Widget::new(ContainerHeader::new(title, spacer));
|
||||
let container = LabeledContainer::new(header, component);
|
||||
let container = LabeledContainer::new(title, component);
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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))
|
||||
.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)
|
||||
}
|
||||
|
|
@ -106,8 +105,7 @@ pub fn workspaces(
|
|||
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);
|
||||
let workspaces = Workspaces::new(tabs, info, children);
|
||||
|
||||
Widget::new(workspaces)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,568 +1,5 @@
|
|||
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, 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
pub mod container;
|
||||
pub mod context;
|
||||
pub mod label;
|
||||
pub mod list;
|
||||
pub mod workspace;
|
||||
|
|
|
|||
|
|
@ -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