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)]
|
||||
pub enum HomeCid {
|
||||
Navigation,
|
||||
Header,
|
||||
Dashboard,
|
||||
IssueBrowser,
|
||||
PatchBrowser,
|
||||
|
|
@ -31,7 +31,7 @@ pub enum HomeCid {
|
|||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum PatchCid {
|
||||
Navigation,
|
||||
Header,
|
||||
Activity,
|
||||
Files,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection};
|
|||
use tuirealm::event::{Event, Key, KeyEvent};
|
||||
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::list::PropertyList;
|
||||
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> {
|
||||
match event {
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
|
|
|
|||
|
|
@ -76,13 +76,14 @@ impl ViewPage for HomeView {
|
|||
context: &Context,
|
||||
theme: &Theme,
|
||||
) -> 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 issue_browser = widget::home::issues(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::IssueBrowser), issue_browser, vec![])?;
|
||||
|
|
@ -94,7 +95,7 @@ impl ViewPage for HomeView {
|
|||
}
|
||||
|
||||
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::IssueBrowser))?;
|
||||
app.umount(&Cid::Home(HomeCid::PatchBrowser))?;
|
||||
|
|
@ -120,13 +121,13 @@ impl ViewPage for HomeView {
|
|||
let area = frame.size();
|
||||
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]);
|
||||
}
|
||||
|
||||
fn subscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||
app.subscribe(
|
||||
&Cid::Home(HomeCid::Navigation),
|
||||
&Cid::Home(HomeCid::Header),
|
||||
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<()> {
|
||||
app.unsubscribe(
|
||||
&Cid::Home(HomeCid::Navigation),
|
||||
&Cid::Home(HomeCid::Header),
|
||||
subscription::navigation_clause(),
|
||||
)?;
|
||||
|
||||
|
|
@ -258,11 +259,13 @@ impl ViewPage for PatchView {
|
|||
theme: &Theme,
|
||||
) -> Result<()> {
|
||||
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 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::Files), files, vec![])?;
|
||||
|
||||
|
|
@ -272,7 +275,7 @@ impl ViewPage for PatchView {
|
|||
}
|
||||
|
||||
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::Files))?;
|
||||
Ok(())
|
||||
|
|
@ -297,13 +300,13 @@ impl ViewPage for PatchView {
|
|||
let area = frame.size();
|
||||
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]);
|
||||
}
|
||||
|
||||
fn subscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||
app.subscribe(
|
||||
&Cid::Patch(PatchCid::Navigation),
|
||||
&Cid::Patch(PatchCid::Header),
|
||||
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<()> {
|
||||
app.unsubscribe(
|
||||
&Cid::Patch(PatchCid::Navigation),
|
||||
&Cid::Patch(PatchCid::Header),
|
||||
subscription::navigation_clause(),
|
||||
)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ use radicle::Profile;
|
|||
|
||||
use radicle::storage::git::Repository;
|
||||
use radicle::storage::ReadStorage;
|
||||
|
||||
pub struct Context {
|
||||
profile: Profile,
|
||||
id: Id,
|
||||
|
|
@ -14,6 +13,7 @@ pub struct Context {
|
|||
impl Context {
|
||||
pub fn new(profile: Profile, id: Id, project: Project) -> Self {
|
||||
let repository = profile.storage.repository(id).unwrap();
|
||||
|
||||
Self {
|
||||
id,
|
||||
profile,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ pub struct IssuePreview {
|
|||
pub shortcuts: Rect,
|
||||
}
|
||||
|
||||
pub struct AppHeader {
|
||||
pub nav: Rect,
|
||||
pub info: Rect,
|
||||
pub line: Rect,
|
||||
}
|
||||
|
||||
pub fn v_stack(
|
||||
widgets: Vec<Box<dyn MockComponent>>,
|
||||
area: Rect,
|
||||
|
|
@ -53,6 +59,30 @@ pub fn h_stack(
|
|||
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> {
|
||||
let nav_h = 3u16;
|
||||
let margin_h = 1u16;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ const COLOR_DEFAULT_FAINT: Color = Color::Rgb(20, 20, 20);
|
|||
pub struct Colors {
|
||||
pub default_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 item_list_highlighted_bg: Color,
|
||||
pub property_name_fg: Color,
|
||||
|
|
@ -79,7 +80,8 @@ pub fn default_dark() -> Theme {
|
|||
colors: Colors {
|
||||
default_fg: COLOR_DEFAULT_FG,
|
||||
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,
|
||||
item_list_highlighted_bg: COLOR_DEFAULT_DARKER,
|
||||
property_name_fg: Color::Cyan,
|
||||
|
|
|
|||
|
|
@ -11,11 +11,12 @@ use context::{Shortcut, Shortcuts};
|
|||
use label::Label;
|
||||
use list::{Property, PropertyList};
|
||||
|
||||
use self::container::Container;
|
||||
use self::container::{AppHeader, AppInfo, Container, VerticalLine};
|
||||
use self::list::{ColumnWidth, PropertyTable};
|
||||
|
||||
use super::Widget;
|
||||
|
||||
use crate::ui::context::Context;
|
||||
use crate::ui::theme::Theme;
|
||||
|
||||
pub fn global_listener() -> Widget<GlobalListener> {
|
||||
|
|
@ -115,10 +116,39 @@ pub fn property_table(_theme: &Theme, properties: Vec<Widget<Property>>) -> Widg
|
|||
Widget::new(table)
|
||||
}
|
||||
|
||||
pub fn tabs(theme: &Theme, tabs: Vec<Widget<Label>>) -> Widget<Tabs> {
|
||||
let line =
|
||||
label(&theme.icons.tab_overline.to_string()).foreground(theme.colors.tabs_highlighted_fg);
|
||||
let tabs = Tabs::new(tabs, line);
|
||||
pub fn tabs(_theme: &Theme, tabs: Vec<Widget<Label>>) -> Widget<Tabs> {
|
||||
let tabs = Tabs::new(tabs);
|
||||
|
||||
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
|
||||
/// by a divider. Highlights the label defined by the current tab index.
|
||||
#[derive(Clone)]
|
||||
pub struct Tabs {
|
||||
tabs: Vec<Widget<Label>>,
|
||||
line: Widget<Label>,
|
||||
state: TabState,
|
||||
}
|
||||
|
||||
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();
|
||||
Self {
|
||||
tabs,
|
||||
line,
|
||||
state: TabState {
|
||||
selected: 0,
|
||||
len: *count as u16,
|
||||
|
|
@ -64,16 +105,7 @@ impl WidgetComponent for Tabs {
|
|||
.unwrap_flag();
|
||||
|
||||
if display {
|
||||
let layout = Layout::default()
|
||||
.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.
|
||||
// Render tabs, highlighting the selected tab.
|
||||
let mut tabs = vec![];
|
||||
for (index, tab) in self.tabs.iter().enumerate() {
|
||||
let mut tab = tab.clone().to_boxed();
|
||||
|
|
@ -87,21 +119,10 @@ impl WidgetComponent for Tabs {
|
|||
}
|
||||
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 {
|
||||
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.
|
||||
pub struct Header<const W: usize> {
|
||||
header: [Widget<Label>; W],
|
||||
|
|
|
|||
Loading…
Reference in New Issue