tui: Implement patches workspace
Signed-off-by: Erik Kundt <erik@zirkular.io>
This commit is contained in:
parent
80c1e45a99
commit
8325eb69d7
|
|
@ -9,27 +9,25 @@ use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers};
|
||||||
use tuirealm::props::{AttrValue, Attribute};
|
use tuirealm::props::{AttrValue, Attribute};
|
||||||
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
||||||
use tuirealm::{
|
use tuirealm::{
|
||||||
Application, Component, Frame, MockComponent, NoUserEvent, Sub, SubClause, SubEventClause,
|
Application, Component, Frame, MockComponent, NoUserEvent, StateValue, Sub, SubClause,
|
||||||
|
SubEventClause,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use radicle_tui::cob::patch::{self};
|
||||||
|
|
||||||
use radicle_tui::ui;
|
use radicle_tui::ui;
|
||||||
use radicle_tui::ui::components::container::{GlobalListener, LabeledContainer};
|
use radicle_tui::ui::components::container::{GlobalListener, LabeledContainer, Tabs};
|
||||||
use radicle_tui::ui::components::context::Shortcuts;
|
use radicle_tui::ui::components::context::Shortcuts;
|
||||||
use radicle_tui::ui::components::list::PropertyList;
|
use radicle_tui::ui::components::list::PropertyList;
|
||||||
use radicle_tui::ui::components::workspace::Workspaces;
|
use radicle_tui::ui::components::workspace::Browser;
|
||||||
use radicle_tui::ui::theme;
|
use radicle_tui::ui::theme;
|
||||||
use radicle_tui::ui::widget::Widget;
|
use radicle_tui::ui::widget::Widget;
|
||||||
|
|
||||||
use radicle_tui::Tui;
|
use radicle_tui::Tui;
|
||||||
|
|
||||||
|
use radicle::cob::patch::{Patch, PatchId};
|
||||||
use radicle::identity::{Id, Project};
|
use radicle::identity::{Id, Project};
|
||||||
|
use radicle::profile::Profile;
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct App {
|
|
||||||
id: Id,
|
|
||||||
project: Project,
|
|
||||||
quit: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Messages handled by this application.
|
/// Messages handled by this application.
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
|
@ -40,18 +38,33 @@ pub enum Message {
|
||||||
/// All components known to this application.
|
/// All components known to this application.
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||||
pub enum ComponentId {
|
pub enum ComponentId {
|
||||||
Workspaces,
|
Navigation,
|
||||||
|
Dashboard,
|
||||||
|
IssueBrowser,
|
||||||
|
PatchBrowser,
|
||||||
Shortcuts,
|
Shortcuts,
|
||||||
GlobalListener,
|
GlobalListener,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct App {
|
||||||
|
profile: Profile,
|
||||||
|
id: Id,
|
||||||
|
project: Project,
|
||||||
|
patches: Vec<(PatchId, Patch)>,
|
||||||
|
quit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a new application using a tui-realm-application, mounts all
|
/// Creates a new application using a tui-realm-application, mounts all
|
||||||
/// components and sets focus to a default one.
|
/// components and sets focus to a default one.
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(id: Id, project: Project) -> Self {
|
pub fn new(profile: Profile, id: Id, project: Project) -> Self {
|
||||||
|
let patches = patch::load_all(&profile, id);
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
|
profile,
|
||||||
project,
|
project,
|
||||||
|
patches,
|
||||||
quit: false,
|
quit: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,56 +74,47 @@ impl Tui<ComponentId, Message> for App {
|
||||||
fn init(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>) -> Result<()> {
|
fn init(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>) -> Result<()> {
|
||||||
let theme = theme::default_dark();
|
let theme = theme::default_dark();
|
||||||
|
|
||||||
let dashboard = ui::labeled_container(
|
let navigation = ui::navigation(&theme).to_boxed();
|
||||||
|
|
||||||
|
let dashboard = ui::dashboard(&theme, &self.id, &self.project).to_boxed();
|
||||||
|
let issue_browser = Box::<Phantom>::default();
|
||||||
|
let patch_browser = ui::patch_browser(&theme, &self.patches, &self.profile).to_boxed();
|
||||||
|
|
||||||
|
let shortcuts = ui::shortcuts(
|
||||||
&theme,
|
&theme,
|
||||||
"about",
|
vec![
|
||||||
ui::property_list(
|
ui::shortcut(&theme, "tab", "section"),
|
||||||
&theme,
|
ui::shortcut(&theme, "q", "quit"),
|
||||||
vec![
|
],
|
||||||
ui::property(&theme, "id", &self.id.to_string()),
|
|
||||||
ui::property(&theme, "name", self.project.name()),
|
|
||||||
ui::property(&theme, "description", self.project.description()),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.to_boxed(),
|
|
||||||
)
|
)
|
||||||
.to_boxed();
|
.to_boxed();
|
||||||
|
|
||||||
|
app.mount(ComponentId::Navigation, navigation, vec![])?;
|
||||||
|
|
||||||
|
app.mount(ComponentId::Dashboard, dashboard, vec![])?;
|
||||||
|
app.mount(ComponentId::IssueBrowser, issue_browser, vec![])?;
|
||||||
app.mount(
|
app.mount(
|
||||||
ComponentId::Workspaces,
|
ComponentId::PatchBrowser,
|
||||||
ui::workspaces(
|
patch_browser,
|
||||||
&theme,
|
vec![
|
||||||
self.project.name(),
|
Sub::new(
|
||||||
ui::tabs(
|
SubEventClause::Keyboard(KeyEvent {
|
||||||
&theme,
|
code: Key::Up,
|
||||||
vec![
|
modifiers: KeyModifiers::NONE,
|
||||||
ui::label("dashboard"),
|
}),
|
||||||
ui::label("issues"),
|
SubClause::Always,
|
||||||
ui::label("patches"),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
vec![
|
Sub::new(
|
||||||
dashboard,
|
SubEventClause::Keyboard(KeyEvent {
|
||||||
Box::<Phantom>::default(),
|
code: Key::Down,
|
||||||
Box::<Phantom>::default(),
|
modifiers: KeyModifiers::NONE,
|
||||||
],
|
}),
|
||||||
)
|
SubClause::Always,
|
||||||
.to_boxed(),
|
),
|
||||||
vec![],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
app.mount(
|
app.mount(ComponentId::Shortcuts, shortcuts, vec![])?;
|
||||||
ComponentId::Shortcuts,
|
|
||||||
ui::shortcuts(
|
|
||||||
&theme,
|
|
||||||
vec![
|
|
||||||
ui::shortcut(&theme, "tab", "section"),
|
|
||||||
ui::shortcut(&theme, "q", "quit"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.to_boxed(),
|
|
||||||
vec![],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Add global key listener and subscribe to key events
|
// Add global key listener and subscribe to key events
|
||||||
app.mount(
|
app.mount(
|
||||||
|
|
@ -126,7 +130,7 @@ impl Tui<ComponentId, Message> for App {
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// We need to give focus to a component then
|
// We need to give focus to a component then
|
||||||
app.active(&ComponentId::Workspaces)?;
|
app.active(&ComponentId::Navigation)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -138,21 +142,25 @@ impl Tui<ComponentId, Message> for App {
|
||||||
) {
|
) {
|
||||||
let area = frame.size();
|
let area = frame.size();
|
||||||
let margin_h = 1u16;
|
let margin_h = 1u16;
|
||||||
|
let navigation_h = 2u16;
|
||||||
let shortcuts_h = app
|
let shortcuts_h = app
|
||||||
.query(&ComponentId::Shortcuts, Attribute::Height)
|
.query(&ComponentId::Shortcuts, Attribute::Height)
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.unwrap_or(AttrValue::Size(0))
|
.unwrap_or(AttrValue::Size(0))
|
||||||
.unwrap_size();
|
.unwrap_size();
|
||||||
let workspaces_h = area
|
let workspaces_h = area.height.saturating_sub(
|
||||||
.height
|
shortcuts_h
|
||||||
.saturating_sub(shortcuts_h.saturating_add(margin_h));
|
.saturating_add(navigation_h)
|
||||||
|
.saturating_add(margin_h),
|
||||||
|
);
|
||||||
|
|
||||||
let layout = Layout::default()
|
let layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.horizontal_margin(margin_h)
|
.horizontal_margin(margin_h)
|
||||||
.constraints(
|
.constraints(
|
||||||
[
|
[
|
||||||
|
Constraint::Length(navigation_h),
|
||||||
Constraint::Length(workspaces_h),
|
Constraint::Length(workspaces_h),
|
||||||
Constraint::Length(shortcuts_h),
|
Constraint::Length(shortcuts_h),
|
||||||
]
|
]
|
||||||
|
|
@ -160,8 +168,21 @@ impl Tui<ComponentId, Message> for App {
|
||||||
)
|
)
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
app.view(&ComponentId::Workspaces, frame, layout[0]);
|
let workspace = app
|
||||||
app.view(&ComponentId::Shortcuts, frame, layout[1]);
|
.state(&ComponentId::Navigation)
|
||||||
|
.unwrap_or(tuirealm::State::One(StateValue::U16(0)))
|
||||||
|
.unwrap_one();
|
||||||
|
|
||||||
|
let component = match workspace {
|
||||||
|
StateValue::U16(0) => ComponentId::Dashboard,
|
||||||
|
StateValue::U16(1) => ComponentId::IssueBrowser,
|
||||||
|
StateValue::U16(2) => ComponentId::PatchBrowser,
|
||||||
|
_ => ComponentId::Dashboard,
|
||||||
|
};
|
||||||
|
|
||||||
|
app.view(&ComponentId::Navigation, frame, layout[0]);
|
||||||
|
app.view(&component, frame, layout[1]);
|
||||||
|
app.view(&ComponentId::Shortcuts, frame, layout[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>, interval: u64) {
|
fn update(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>, interval: u64) {
|
||||||
|
|
@ -194,7 +215,7 @@ impl Component<Message, NoUserEvent> for Widget<GlobalListener> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Message, NoUserEvent> for Widget<Workspaces> {
|
impl Component<Message, NoUserEvent> for Widget<Tabs> {
|
||||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
match event {
|
match event {
|
||||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||||
|
|
@ -206,6 +227,24 @@ impl Component<Message, NoUserEvent> for Widget<Workspaces> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Component<Message, NoUserEvent> for Widget<Browser<(PatchId, Patch)>> {
|
||||||
|
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
|
match event {
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||||
|
self.perform(Cmd::Move(MoveDirection::Up));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent {
|
||||||
|
code: Key::Down, ..
|
||||||
|
}) => {
|
||||||
|
self.perform(Cmd::Move(MoveDirection::Down));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Component<Message, NoUserEvent> for Widget<LabeledContainer> {
|
impl Component<Message, NoUserEvent> for Widget<LabeledContainer> {
|
||||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
None
|
None
|
||||||
|
|
@ -223,3 +262,9 @@ impl Component<Message, NoUserEvent> for Widget<Shortcuts> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Component<Message, NoUserEvent> for Phantom {
|
||||||
|
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use tuirealm::terminal::TerminalBridge;
|
||||||
use tuirealm::Frame;
|
use tuirealm::Frame;
|
||||||
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
|
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
|
||||||
|
|
||||||
|
pub mod cob;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
/// Trait that must be implemented by client applications in order to be run
|
/// Trait that must be implemented by client applications in order to be run
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ fn execute() -> anyhow::Result<()> {
|
||||||
let project = payload.project()?;
|
let project = payload.project()?;
|
||||||
|
|
||||||
let mut window = Window::default();
|
let mut window = Window::default();
|
||||||
window.run(&mut app::App::new(id, project), 1000 / FPS)?;
|
window.run(&mut app::App::new(profile, id, project), 1000 / FPS)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,26 @@
|
||||||
|
pub mod cob;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
pub mod widget;
|
pub mod widget;
|
||||||
|
|
||||||
|
use radicle::prelude::{Id, Project};
|
||||||
|
use radicle::Profile;
|
||||||
|
use tuirealm::props::{AttrValue, Attribute, PropPayload, PropValue, TextSpan};
|
||||||
|
use tuirealm::MockComponent;
|
||||||
|
|
||||||
|
use radicle::cob::patch::{Patch, PatchId};
|
||||||
|
|
||||||
use components::container::{GlobalListener, LabeledContainer, Tabs};
|
use components::container::{GlobalListener, LabeledContainer, Tabs};
|
||||||
use components::context::{Shortcut, Shortcuts};
|
use components::context::{Shortcut, Shortcuts};
|
||||||
use components::label::Label;
|
use components::label::Label;
|
||||||
use components::list::{Property, PropertyList};
|
use components::list::{Property, PropertyList};
|
||||||
use components::workspace::Workspaces;
|
|
||||||
use widget::Widget;
|
use widget::Widget;
|
||||||
|
|
||||||
use tuirealm::props::{AttrValue, Attribute};
|
use self::components::list::{List, Table};
|
||||||
use tuirealm::MockComponent;
|
use self::components::workspace::Browser;
|
||||||
|
|
||||||
pub fn global_listener() -> Widget<GlobalListener> {
|
pub fn global_listener() -> Widget<GlobalListener> {
|
||||||
Widget::new(GlobalListener::default())
|
Widget::new(GlobalListener::default())
|
||||||
|
|
@ -100,14 +108,65 @@ pub fn tabs(theme: &theme::Theme, tabs: Vec<Widget<Label>>) -> Widget<Tabs> {
|
||||||
.highlight(theme.colors.tabs_highlighted_fg)
|
.highlight(theme.colors.tabs_highlighted_fg)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn workspaces(
|
pub fn table(theme: &theme::Theme, items: &[impl List], profile: &Profile) -> Widget<Table> {
|
||||||
theme: &theme::Theme,
|
let table = Table::default();
|
||||||
info: &str,
|
let items = items.iter().map(|item| item.row(theme, profile)).collect();
|
||||||
tabs: Widget<Tabs>,
|
|
||||||
children: Vec<Box<dyn MockComponent>>,
|
|
||||||
) -> Widget<Workspaces> {
|
|
||||||
let info = label(info).foreground(theme.colors.workspaces_info_fg);
|
|
||||||
let workspaces = Workspaces::new(tabs, info, children);
|
|
||||||
|
|
||||||
Widget::new(workspaces)
|
Widget::new(table)
|
||||||
|
.content(AttrValue::Table(items))
|
||||||
|
.background(theme.colors.labeled_container_bg)
|
||||||
|
.highlight(theme.colors.item_list_highlighted_bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn patch_browser(
|
||||||
|
theme: &theme::Theme,
|
||||||
|
items: &[(PatchId, Patch)],
|
||||||
|
profile: &Profile,
|
||||||
|
) -> Widget<Browser<(PatchId, Patch)>> {
|
||||||
|
let widths = AttrValue::Payload(PropPayload::Vec(vec![
|
||||||
|
PropValue::U16(2),
|
||||||
|
PropValue::U16(43),
|
||||||
|
PropValue::U16(15),
|
||||||
|
PropValue::U16(15),
|
||||||
|
PropValue::U16(5),
|
||||||
|
PropValue::U16(20),
|
||||||
|
]));
|
||||||
|
let header = AttrValue::Payload(PropPayload::Vec(vec![
|
||||||
|
PropValue::TextSpan(TextSpan::from("")),
|
||||||
|
PropValue::TextSpan(TextSpan::from("title")),
|
||||||
|
PropValue::TextSpan(TextSpan::from("author")),
|
||||||
|
PropValue::TextSpan(TextSpan::from("tags")),
|
||||||
|
PropValue::TextSpan(TextSpan::from("comments")),
|
||||||
|
PropValue::TextSpan(TextSpan::from("date")),
|
||||||
|
]));
|
||||||
|
|
||||||
|
let table = table(theme, items, profile)
|
||||||
|
.custom("widths", widths)
|
||||||
|
.custom("header", header);
|
||||||
|
let browser: Browser<(PatchId, Patch)> = Browser::new(table);
|
||||||
|
|
||||||
|
Widget::new(browser)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn navigation(theme: &theme::Theme) -> Widget<Tabs> {
|
||||||
|
tabs(
|
||||||
|
theme,
|
||||||
|
vec![label("dashboard"), label("issues"), label("patches")],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dashboard(theme: &theme::Theme, id: &Id, project: &Project) -> Widget<LabeledContainer> {
|
||||||
|
labeled_container(
|
||||||
|
theme,
|
||||||
|
"about",
|
||||||
|
property_list(
|
||||||
|
theme,
|
||||||
|
vec![
|
||||||
|
property(theme, "id", &id.to_string()),
|
||||||
|
property(theme, "name", project.name()),
|
||||||
|
property(theme, "description", project.description()),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.to_boxed(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,132 +1,43 @@
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use tuirealm::command::{Cmd, CmdResult};
|
use tuirealm::command::{Cmd, CmdResult};
|
||||||
use tuirealm::props::{AttrValue, Attribute, Props};
|
use tuirealm::props::Props;
|
||||||
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
|
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
|
||||||
use tuirealm::{Frame, MockComponent, State};
|
use tuirealm::{Frame, MockComponent, State};
|
||||||
|
|
||||||
use crate::ui::components::label::Label;
|
|
||||||
use crate::ui::widget::{Widget, WidgetComponent};
|
use crate::ui::widget::{Widget, WidgetComponent};
|
||||||
|
|
||||||
use super::container::Tabs;
|
use super::list::{List, Table};
|
||||||
|
|
||||||
/// Workspace header that displays all labels horizontally aligned and separated
|
pub struct Browser<T> {
|
||||||
/// by a divider. Highlights the label defined by the current tab index.
|
list: Widget<Table>,
|
||||||
#[derive(Clone)]
|
phantom: PhantomData<T>,
|
||||||
struct Header {
|
|
||||||
tabs: Widget<Tabs>,
|
|
||||||
info: Widget<Label>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Header {
|
impl<T: List> Browser<T> {
|
||||||
pub fn new(tabs: Widget<Tabs>, info: Widget<Label>) -> Self {
|
pub fn new(list: Widget<Table>) -> 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 {
|
Self {
|
||||||
header: Widget::new(Header::new(tabs, info)),
|
list,
|
||||||
children,
|
phantom: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetComponent for Workspaces {
|
impl<T: List> WidgetComponent for Browser<T> {
|
||||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
|
||||||
let display = properties
|
let layout = Layout::default()
|
||||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
.direction(Direction::Vertical)
|
||||||
.unwrap_flag();
|
.constraints(vec![Constraint::Min(1)].as_ref())
|
||||||
let header_height = self
|
.split(area);
|
||||||
.header
|
|
||||||
.query(Attribute::Height)
|
|
||||||
.unwrap_or(AttrValue::Size(1))
|
|
||||||
.unwrap_size();
|
|
||||||
let selected = self.header.tabs.state().unwrap_one().unwrap_u16();
|
|
||||||
|
|
||||||
if display {
|
self.list.view(frame, layout[0]);
|
||||||
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 {
|
fn state(&self) -> State {
|
||||||
State::None
|
State::None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
|
||||||
CmdResult::Batch(
|
self.list.perform(cmd)
|
||||||
[
|
|
||||||
self.children
|
|
||||||
.iter_mut()
|
|
||||||
.map(|child| child.perform(cmd))
|
|
||||||
.collect(),
|
|
||||||
vec![self.header.perform(cmd)],
|
|
||||||
]
|
|
||||||
.concat(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue