tui: Improve application header
This commit is contained in:
parent
8811fa97d4
commit
21950e1cae
|
|
@ -23,7 +23,7 @@ use self::page::{IssuePage, PageStack};
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||||
pub enum HomeCid {
|
pub enum HomeCid {
|
||||||
Navigation,
|
Header,
|
||||||
Dashboard,
|
Dashboard,
|
||||||
IssueBrowser,
|
IssueBrowser,
|
||||||
PatchBrowser,
|
PatchBrowser,
|
||||||
|
|
@ -31,7 +31,7 @@ pub enum HomeCid {
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||||
pub enum PatchCid {
|
pub enum PatchCid {
|
||||||
Navigation,
|
Header,
|
||||||
Activity,
|
Activity,
|
||||||
Files,
|
Files,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection};
|
||||||
use tuirealm::event::{Event, Key, KeyEvent};
|
use tuirealm::event::{Event, Key, KeyEvent};
|
||||||
use tuirealm::{MockComponent, NoUserEvent, State, StateValue};
|
use tuirealm::{MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
use radicle_tui::ui::widget::common::container::{GlobalListener, LabeledContainer, Tabs};
|
use radicle_tui::ui::widget::common::container::{AppHeader, GlobalListener, LabeledContainer};
|
||||||
use radicle_tui::ui::widget::common::context::{ContextBar, Shortcuts};
|
use radicle_tui::ui::widget::common::context::{ContextBar, Shortcuts};
|
||||||
use radicle_tui::ui::widget::common::list::PropertyList;
|
use radicle_tui::ui::widget::common::list::PropertyList;
|
||||||
use radicle_tui::ui::widget::home::{Dashboard, IssueBrowser, PatchBrowser};
|
use radicle_tui::ui::widget::home::{Dashboard, IssueBrowser, PatchBrowser};
|
||||||
|
|
@ -29,7 +29,7 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<GlobalListener> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<Tabs> {
|
impl tuirealm::Component<Message, NoUserEvent> for Widget<AppHeader> {
|
||||||
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, .. }) => {
|
||||||
|
|
|
||||||
|
|
@ -76,13 +76,14 @@ impl ViewPage for HomeView {
|
||||||
context: &Context,
|
context: &Context,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let navigation = widget::home::navigation(theme).to_boxed();
|
let navigation = widget::home::navigation(theme);
|
||||||
|
let header = widget::common::app_header(context, theme, Some(navigation)).to_boxed();
|
||||||
|
|
||||||
let dashboard = widget::home::dashboard(context, theme).to_boxed();
|
let dashboard = widget::home::dashboard(context, theme).to_boxed();
|
||||||
let issue_browser = widget::home::issues(context, theme).to_boxed();
|
let issue_browser = widget::home::issues(context, theme).to_boxed();
|
||||||
let patch_browser = widget::home::patches(context, theme).to_boxed();
|
let patch_browser = widget::home::patches(context, theme).to_boxed();
|
||||||
|
|
||||||
app.remount(Cid::Home(HomeCid::Navigation), navigation, vec![])?;
|
app.remount(Cid::Home(HomeCid::Header), header, vec![])?;
|
||||||
|
|
||||||
app.remount(Cid::Home(HomeCid::Dashboard), dashboard, vec![])?;
|
app.remount(Cid::Home(HomeCid::Dashboard), dashboard, vec![])?;
|
||||||
app.remount(Cid::Home(HomeCid::IssueBrowser), issue_browser, vec![])?;
|
app.remount(Cid::Home(HomeCid::IssueBrowser), issue_browser, vec![])?;
|
||||||
|
|
@ -94,7 +95,7 @@ impl ViewPage for HomeView {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unmount(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
fn unmount(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||||
app.umount(&Cid::Home(HomeCid::Navigation))?;
|
app.umount(&Cid::Home(HomeCid::Header))?;
|
||||||
app.umount(&Cid::Home(HomeCid::Dashboard))?;
|
app.umount(&Cid::Home(HomeCid::Dashboard))?;
|
||||||
app.umount(&Cid::Home(HomeCid::IssueBrowser))?;
|
app.umount(&Cid::Home(HomeCid::IssueBrowser))?;
|
||||||
app.umount(&Cid::Home(HomeCid::PatchBrowser))?;
|
app.umount(&Cid::Home(HomeCid::PatchBrowser))?;
|
||||||
|
|
@ -120,13 +121,13 @@ impl ViewPage for HomeView {
|
||||||
let area = frame.size();
|
let area = frame.size();
|
||||||
let layout = layout::default_page(area);
|
let layout = layout::default_page(area);
|
||||||
|
|
||||||
app.view(&Cid::Home(HomeCid::Navigation), frame, layout[0]);
|
app.view(&Cid::Home(HomeCid::Header), frame, layout[0]);
|
||||||
app.view(&self.active_component, frame, layout[1]);
|
app.view(&self.active_component, frame, layout[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
fn subscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||||
app.subscribe(
|
app.subscribe(
|
||||||
&Cid::Home(HomeCid::Navigation),
|
&Cid::Home(HomeCid::Header),
|
||||||
Sub::new(subscription::navigation_clause(), SubClause::Always),
|
Sub::new(subscription::navigation_clause(), SubClause::Always),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
@ -135,7 +136,7 @@ impl ViewPage for HomeView {
|
||||||
|
|
||||||
fn unsubscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
fn unsubscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||||
app.unsubscribe(
|
app.unsubscribe(
|
||||||
&Cid::Home(HomeCid::Navigation),
|
&Cid::Home(HomeCid::Header),
|
||||||
subscription::navigation_clause(),
|
subscription::navigation_clause(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
@ -258,11 +259,13 @@ impl ViewPage for PatchView {
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (id, patch) = &self.patch;
|
let (id, patch) = &self.patch;
|
||||||
let navigation = widget::patch::navigation(theme).to_boxed();
|
let navigation = widget::patch::navigation(theme);
|
||||||
|
let header = widget::common::app_header(context, theme, Some(navigation)).to_boxed();
|
||||||
|
|
||||||
let activity = widget::patch::activity(theme, (*id, patch), context.profile()).to_boxed();
|
let activity = widget::patch::activity(theme, (*id, patch), context.profile()).to_boxed();
|
||||||
let files = widget::patch::files(theme, (*id, patch), context.profile()).to_boxed();
|
let files = widget::patch::files(theme, (*id, patch), context.profile()).to_boxed();
|
||||||
|
|
||||||
app.remount(Cid::Patch(PatchCid::Navigation), navigation, vec![])?;
|
app.remount(Cid::Patch(PatchCid::Header), header, vec![])?;
|
||||||
app.remount(Cid::Patch(PatchCid::Activity), activity, vec![])?;
|
app.remount(Cid::Patch(PatchCid::Activity), activity, vec![])?;
|
||||||
app.remount(Cid::Patch(PatchCid::Files), files, vec![])?;
|
app.remount(Cid::Patch(PatchCid::Files), files, vec![])?;
|
||||||
|
|
||||||
|
|
@ -272,7 +275,7 @@ impl ViewPage for PatchView {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unmount(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
fn unmount(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||||
app.umount(&Cid::Patch(PatchCid::Navigation))?;
|
app.umount(&Cid::Patch(PatchCid::Header))?;
|
||||||
app.umount(&Cid::Patch(PatchCid::Activity))?;
|
app.umount(&Cid::Patch(PatchCid::Activity))?;
|
||||||
app.umount(&Cid::Patch(PatchCid::Files))?;
|
app.umount(&Cid::Patch(PatchCid::Files))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -297,13 +300,13 @@ impl ViewPage for PatchView {
|
||||||
let area = frame.size();
|
let area = frame.size();
|
||||||
let layout = layout::default_page(area);
|
let layout = layout::default_page(area);
|
||||||
|
|
||||||
app.view(&Cid::Patch(PatchCid::Navigation), frame, layout[0]);
|
app.view(&Cid::Patch(PatchCid::Header), frame, layout[0]);
|
||||||
app.view(&self.active_component, frame, layout[1]);
|
app.view(&self.active_component, frame, layout[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
fn subscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||||
app.subscribe(
|
app.subscribe(
|
||||||
&Cid::Patch(PatchCid::Navigation),
|
&Cid::Patch(PatchCid::Header),
|
||||||
Sub::new(subscription::navigation_clause(), SubClause::Always),
|
Sub::new(subscription::navigation_clause(), SubClause::Always),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
@ -312,7 +315,7 @@ impl ViewPage for PatchView {
|
||||||
|
|
||||||
fn unsubscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
fn unsubscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||||
app.unsubscribe(
|
app.unsubscribe(
|
||||||
&Cid::Patch(PatchCid::Navigation),
|
&Cid::Patch(PatchCid::Header),
|
||||||
subscription::navigation_clause(),
|
subscription::navigation_clause(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ use radicle::Profile;
|
||||||
|
|
||||||
use radicle::storage::git::Repository;
|
use radicle::storage::git::Repository;
|
||||||
use radicle::storage::ReadStorage;
|
use radicle::storage::ReadStorage;
|
||||||
|
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
profile: Profile,
|
profile: Profile,
|
||||||
id: Id,
|
id: Id,
|
||||||
|
|
@ -14,6 +13,7 @@ pub struct Context {
|
||||||
impl Context {
|
impl Context {
|
||||||
pub fn new(profile: Profile, id: Id, project: Project) -> Self {
|
pub fn new(profile: Profile, id: Id, project: Project) -> Self {
|
||||||
let repository = profile.storage.repository(id).unwrap();
|
let repository = profile.storage.repository(id).unwrap();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
profile,
|
profile,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,12 @@ pub struct IssuePreview {
|
||||||
pub shortcuts: Rect,
|
pub shortcuts: Rect,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AppHeader {
|
||||||
|
pub nav: Rect,
|
||||||
|
pub info: Rect,
|
||||||
|
pub line: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn v_stack(
|
pub fn v_stack(
|
||||||
widgets: Vec<Box<dyn MockComponent>>,
|
widgets: Vec<Box<dyn MockComponent>>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
|
|
@ -53,6 +59,30 @@ pub fn h_stack(
|
||||||
widgets.into_iter().zip(layout.into_iter()).collect()
|
widgets.into_iter().zip(layout.into_iter()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn app_header(area: Rect, info_w: u16) -> AppHeader {
|
||||||
|
let nav_w = area.width.saturating_sub(info_w);
|
||||||
|
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(vec![
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(1),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let top = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Length(nav_w), Constraint::Length(info_w)].as_ref())
|
||||||
|
.split(layout[1]);
|
||||||
|
|
||||||
|
AppHeader {
|
||||||
|
nav: top[0],
|
||||||
|
info: top[1],
|
||||||
|
line: layout[2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_page(area: Rect) -> Vec<Rect> {
|
pub fn default_page(area: Rect) -> Vec<Rect> {
|
||||||
let nav_h = 3u16;
|
let nav_h = 3u16;
|
||||||
let margin_h = 1u16;
|
let margin_h = 1u16;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ const COLOR_DEFAULT_FAINT: Color = Color::Rgb(20, 20, 20);
|
||||||
pub struct Colors {
|
pub struct Colors {
|
||||||
pub default_fg: Color,
|
pub default_fg: Color,
|
||||||
pub tabs_highlighted_fg: Color,
|
pub tabs_highlighted_fg: Color,
|
||||||
pub workspaces_info_fg: Color,
|
pub app_header_project_fg: Color,
|
||||||
|
pub app_header_rid_fg: Color,
|
||||||
pub labeled_container_bg: Color,
|
pub labeled_container_bg: Color,
|
||||||
pub item_list_highlighted_bg: Color,
|
pub item_list_highlighted_bg: Color,
|
||||||
pub property_name_fg: Color,
|
pub property_name_fg: Color,
|
||||||
|
|
@ -79,7 +80,8 @@ pub fn default_dark() -> Theme {
|
||||||
colors: Colors {
|
colors: Colors {
|
||||||
default_fg: COLOR_DEFAULT_FG,
|
default_fg: COLOR_DEFAULT_FG,
|
||||||
tabs_highlighted_fg: Color::Magenta,
|
tabs_highlighted_fg: Color::Magenta,
|
||||||
workspaces_info_fg: Color::Yellow,
|
app_header_project_fg: Color::Cyan,
|
||||||
|
app_header_rid_fg: Color::Yellow,
|
||||||
labeled_container_bg: COLOR_DEFAULT_FAINT,
|
labeled_container_bg: COLOR_DEFAULT_FAINT,
|
||||||
item_list_highlighted_bg: COLOR_DEFAULT_DARKER,
|
item_list_highlighted_bg: COLOR_DEFAULT_DARKER,
|
||||||
property_name_fg: Color::Cyan,
|
property_name_fg: Color::Cyan,
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,12 @@ use context::{Shortcut, Shortcuts};
|
||||||
use label::Label;
|
use label::Label;
|
||||||
use list::{Property, PropertyList};
|
use list::{Property, PropertyList};
|
||||||
|
|
||||||
use self::container::Container;
|
use self::container::{AppHeader, AppInfo, Container, VerticalLine};
|
||||||
use self::list::{ColumnWidth, PropertyTable};
|
use self::list::{ColumnWidth, PropertyTable};
|
||||||
|
|
||||||
use super::Widget;
|
use super::Widget;
|
||||||
|
|
||||||
|
use crate::ui::context::Context;
|
||||||
use crate::ui::theme::Theme;
|
use crate::ui::theme::Theme;
|
||||||
|
|
||||||
pub fn global_listener() -> Widget<GlobalListener> {
|
pub fn global_listener() -> Widget<GlobalListener> {
|
||||||
|
|
@ -115,10 +116,39 @@ pub fn property_table(_theme: &Theme, properties: Vec<Widget<Property>>) -> Widg
|
||||||
Widget::new(table)
|
Widget::new(table)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tabs(theme: &Theme, tabs: Vec<Widget<Label>>) -> Widget<Tabs> {
|
pub fn tabs(_theme: &Theme, tabs: Vec<Widget<Label>>) -> Widget<Tabs> {
|
||||||
let line =
|
let tabs = Tabs::new(tabs);
|
||||||
label(&theme.icons.tab_overline.to_string()).foreground(theme.colors.tabs_highlighted_fg);
|
|
||||||
let tabs = Tabs::new(tabs, line);
|
|
||||||
|
|
||||||
Widget::new(tabs).height(2)
|
Widget::new(tabs).height(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn app_info(context: &Context, theme: &Theme) -> Widget<AppInfo> {
|
||||||
|
let project = label(context.project().name()).foreground(theme.colors.app_header_project_fg);
|
||||||
|
let rid = label(&format!(" ({})", context.id())).foreground(theme.colors.app_header_rid_fg);
|
||||||
|
|
||||||
|
let project_w = project
|
||||||
|
.query(Attribute::Width)
|
||||||
|
.unwrap_or(AttrValue::Size(0))
|
||||||
|
.unwrap_size();
|
||||||
|
let rid_w = rid
|
||||||
|
.query(Attribute::Width)
|
||||||
|
.unwrap_or(AttrValue::Size(0))
|
||||||
|
.unwrap_size();
|
||||||
|
|
||||||
|
let info = AppInfo::new(project, rid);
|
||||||
|
Widget::new(info).width(project_w.saturating_add(rid_w))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn app_header(
|
||||||
|
context: &Context,
|
||||||
|
theme: &Theme,
|
||||||
|
nav: Option<Widget<Tabs>>,
|
||||||
|
) -> Widget<AppHeader> {
|
||||||
|
let line =
|
||||||
|
label(&theme.icons.tab_overline.to_string()).foreground(theme.colors.tabs_highlighted_fg);
|
||||||
|
let line = Widget::new(VerticalLine::new(line));
|
||||||
|
let info = app_info(context, theme);
|
||||||
|
let header = AppHeader::new(nav, info, line);
|
||||||
|
|
||||||
|
Widget::new(header)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,21 +33,62 @@ impl WidgetComponent for GlobalListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A vertical separator.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct VerticalLine {
|
||||||
|
line: Widget<Label>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VerticalLine {
|
||||||
|
pub fn new(line: Widget<Label>) -> Self {
|
||||||
|
Self { line }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetComponent for VerticalLine {
|
||||||
|
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 {
|
||||||
|
// Repeat and render line.
|
||||||
|
let overlines = vec![self.line.clone(); area.width as usize];
|
||||||
|
let overlines = overlines
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.clone().to_boxed() as Box<dyn MockComponent>)
|
||||||
|
.collect();
|
||||||
|
let line_layout = layout::h_stack(overlines, area);
|
||||||
|
for (mut line, area) in line_layout {
|
||||||
|
line.view(frame, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
State::None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////
|
||||||
|
|
||||||
/// A tab header that displays all labels horizontally aligned and separated
|
/// A tab header that displays all labels horizontally aligned and separated
|
||||||
/// by a divider. Highlights the label defined by the current tab index.
|
/// by a divider. Highlights the label defined by the current tab index.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Tabs {
|
pub struct Tabs {
|
||||||
tabs: Vec<Widget<Label>>,
|
tabs: Vec<Widget<Label>>,
|
||||||
line: Widget<Label>,
|
|
||||||
state: TabState,
|
state: TabState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tabs {
|
impl Tabs {
|
||||||
pub fn new(tabs: Vec<Widget<Label>>, line: Widget<Label>) -> Self {
|
pub fn new(tabs: Vec<Widget<Label>>) -> Self {
|
||||||
let count = &tabs.len();
|
let count = &tabs.len();
|
||||||
Self {
|
Self {
|
||||||
tabs,
|
tabs,
|
||||||
line,
|
|
||||||
state: TabState {
|
state: TabState {
|
||||||
selected: 0,
|
selected: 0,
|
||||||
len: *count as u16,
|
len: *count as u16,
|
||||||
|
|
@ -64,16 +105,7 @@ impl WidgetComponent for Tabs {
|
||||||
.unwrap_flag();
|
.unwrap_flag();
|
||||||
|
|
||||||
if display {
|
if display {
|
||||||
let layout = Layout::default()
|
// Render tabs, highlighting the selected tab.
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(vec![
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
])
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
// Render tabs on first row, highlighting the selected tab.
|
|
||||||
let mut tabs = vec![];
|
let mut tabs = vec![];
|
||||||
for (index, tab) in self.tabs.iter().enumerate() {
|
for (index, tab) in self.tabs.iter().enumerate() {
|
||||||
let mut tab = tab.clone().to_boxed();
|
let mut tab = tab.clone().to_boxed();
|
||||||
|
|
@ -87,21 +119,10 @@ impl WidgetComponent for Tabs {
|
||||||
}
|
}
|
||||||
tabs.push(Widget::new(Label::default()).to_boxed());
|
tabs.push(Widget::new(Label::default()).to_boxed());
|
||||||
|
|
||||||
let tab_layout = layout::h_stack(tabs, layout[1]);
|
let tab_layout = layout::h_stack(tabs, area);
|
||||||
for (mut tab, area) in tab_layout {
|
for (mut tab, area) in tab_layout {
|
||||||
tab.view(frame, area);
|
tab.view(frame, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repeat and render line on second row.
|
|
||||||
let overlines = vec![self.line.clone(); area.width as usize];
|
|
||||||
let overlines = overlines
|
|
||||||
.iter()
|
|
||||||
.map(|l| l.clone().to_boxed() as Box<dyn MockComponent>)
|
|
||||||
.collect();
|
|
||||||
let line_layout = layout::h_stack(overlines, layout[2]);
|
|
||||||
for (mut line, area) in line_layout {
|
|
||||||
line.view(frame, area);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,6 +148,112 @@ impl WidgetComponent for Tabs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An application info widget that renders project / branch information
|
||||||
|
/// and a separator line. Used in conjunction with [`Tabs`].
|
||||||
|
pub struct AppInfo {
|
||||||
|
project: Widget<Label>,
|
||||||
|
rid: Widget<Label>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppInfo {
|
||||||
|
pub fn new(project: Widget<Label>, rid: Widget<Label>) -> Self {
|
||||||
|
Self { project, rid }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetComponent for AppInfo {
|
||||||
|
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
||||||
|
let display = properties
|
||||||
|
.get_or(Attribute::Display, AttrValue::Flag(true))
|
||||||
|
.unwrap_flag();
|
||||||
|
|
||||||
|
let project_w = self
|
||||||
|
.project
|
||||||
|
.query(Attribute::Width)
|
||||||
|
.unwrap_or(AttrValue::Size(10))
|
||||||
|
.unwrap_size();
|
||||||
|
|
||||||
|
let rid_w = self
|
||||||
|
.rid
|
||||||
|
.query(Attribute::Width)
|
||||||
|
.unwrap_or(AttrValue::Size(10))
|
||||||
|
.unwrap_size();
|
||||||
|
|
||||||
|
if display {
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(vec![
|
||||||
|
Constraint::Length(project_w),
|
||||||
|
Constraint::Length(rid_w),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
self.project.view(frame, layout[0]);
|
||||||
|
self.rid.view(frame, layout[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
State::None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A common application header that renders project / branch
|
||||||
|
/// information and an optional navigation.
|
||||||
|
pub struct AppHeader {
|
||||||
|
nav: Option<Widget<Tabs>>,
|
||||||
|
info: Widget<AppInfo>,
|
||||||
|
line: Widget<VerticalLine>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppHeader {
|
||||||
|
pub fn new(
|
||||||
|
nav: Option<Widget<Tabs>>,
|
||||||
|
info: Widget<AppInfo>,
|
||||||
|
line: Widget<VerticalLine>,
|
||||||
|
) -> Self {
|
||||||
|
Self { nav, info, line }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetComponent for AppHeader {
|
||||||
|
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_w = self
|
||||||
|
.info
|
||||||
|
.query(Attribute::Width)
|
||||||
|
.unwrap_or(AttrValue::Size(10))
|
||||||
|
.unwrap_size();
|
||||||
|
|
||||||
|
if display {
|
||||||
|
let layout = layout::app_header(area, info_w);
|
||||||
|
|
||||||
|
if let Some(nav) = self.nav.as_mut() {
|
||||||
|
nav.view(frame, layout.nav);
|
||||||
|
}
|
||||||
|
self.info.view(frame, layout.info);
|
||||||
|
self.line.view(frame, layout.line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
State::None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
|
||||||
|
self.nav
|
||||||
|
.as_mut()
|
||||||
|
.map(|nav| nav.perform(cmd))
|
||||||
|
.unwrap_or(CmdResult::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A labeled container header.
|
/// A labeled container header.
|
||||||
pub struct Header<const W: usize> {
|
pub struct Header<const W: usize> {
|
||||||
header: [Widget<Label>; W],
|
header: [Widget<Label>; W],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue