mirror of
https://seed.radicle.dev/rad:z39mP9rQAaGmERfUMPULfPUi473tY.git
synced 2026-10-18 19:10:36 +08:00
lib/ui: Remove imUI specialization
This commit is contained in:
+14
-16
@@ -6,8 +6,7 @@ use std::vec;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use ratatui::layout::Constraint;
|
||||
use ratatui::layout::Layout;
|
||||
use ratatui::layout::{Constraint, Layout};
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::text::Span;
|
||||
use ratatui::{Frame, Viewport};
|
||||
@@ -22,10 +21,9 @@ use radicle_tui as tui;
|
||||
use tui::event::Key;
|
||||
use tui::store;
|
||||
use tui::task::{Process, Task};
|
||||
use tui::ui::im;
|
||||
use tui::ui::im::widget::{ContainerState, TableState, TextEditState, TextViewState, Window};
|
||||
use tui::ui::im::{Borders, Show};
|
||||
use tui::ui::{BufferedValue, Column, Spacing};
|
||||
use tui::ui;
|
||||
use tui::ui::widget::{ContainerState, TableState, TextEditState, TextViewState, Window};
|
||||
use tui::ui::{Borders, BufferedValue, Column, Show, Spacing, Ui};
|
||||
use tui::{Channel, Exit};
|
||||
|
||||
use super::common::RepositoryMode;
|
||||
@@ -262,7 +260,7 @@ impl store::Update<Message> for App {
|
||||
}
|
||||
|
||||
impl Show<Message> for App {
|
||||
fn show(&self, ctx: &im::Context<Message>, frame: &mut Frame) -> Result<()> {
|
||||
fn show(&self, ctx: &ui::Context<Message>, frame: &mut Frame) -> Result<()> {
|
||||
Window::default().show(ctx, |ui| {
|
||||
// Initialize
|
||||
if !self.state.initialized {
|
||||
@@ -281,7 +279,7 @@ impl Show<Message> for App {
|
||||
let mut group_focus = self.state.main_group.focus();
|
||||
|
||||
let group = ui.container(
|
||||
im::Layout::Expandable3 { left_only: true },
|
||||
ui::Layout::Expandable3 { left_only: true },
|
||||
&mut group_focus,
|
||||
|ui| {
|
||||
self.show_browser(frame, ui);
|
||||
@@ -338,7 +336,7 @@ impl Show<Message> for App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn show_browser(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_browser(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
let context = self.context.lock().unwrap();
|
||||
let notifs = self.notifications.lock().unwrap();
|
||||
let notifs = notifs
|
||||
@@ -415,14 +413,14 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_browser_footer(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
fn show_browser_footer(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
ui.layout(Layout::vertical([3, 1]), None, |ui| {
|
||||
self.show_browser_context(frame, ui);
|
||||
self.show_browser_shortcuts(frame, ui);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
let (mut search_text, mut search_cursor) = (
|
||||
self.state.search.clone().read().text,
|
||||
self.state.search.clone().read().cursor,
|
||||
@@ -453,7 +451,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_browser_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
fn show_browser_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
let context = {
|
||||
let notifs = self.notifications.lock().unwrap();
|
||||
let search = self.state.search.read().text;
|
||||
@@ -554,7 +552,7 @@ impl App {
|
||||
ui.column_bar(frame, context, Spacing::from(0), Some(Borders::None));
|
||||
}
|
||||
|
||||
pub fn show_browser_shortcuts(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_browser_shortcuts(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
ui.shortcuts(
|
||||
frame,
|
||||
&[
|
||||
@@ -569,7 +567,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
|
||||
fn show_loading_popup(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
fn show_loading_popup(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
ui.popup(Layout::vertical([Constraint::Min(1)]), |ui| {
|
||||
ui.layout(
|
||||
Layout::vertical([Constraint::Min(1), Constraint::Length(3)]).margin(1),
|
||||
@@ -599,7 +597,7 @@ impl App {
|
||||
});
|
||||
}
|
||||
|
||||
fn show_help_text(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
fn show_help_text(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
ui.column_bar(
|
||||
frame,
|
||||
[Column::new(Span::raw(" Help ").bold(), Constraint::Fill(1))].to_vec(),
|
||||
@@ -621,7 +619,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_help_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
fn show_help_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
ui.column_bar(
|
||||
frame,
|
||||
[
|
||||
|
||||
+20
-21
@@ -4,8 +4,6 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use radicle_tui::ui::im::widget::TreeState;
|
||||
use radicle_tui::ui::ToRow;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Position};
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::{Line, Span, Text};
|
||||
@@ -21,11 +19,12 @@ use radicle_tui as tui;
|
||||
use tui::event::Key;
|
||||
use tui::store;
|
||||
use tui::task::EmptyProcessors;
|
||||
use tui::ui::im;
|
||||
use tui::ui::im::widget::{ContainerState, TableState, TextEditState, TextViewState, Window};
|
||||
use tui::ui::im::{Borders, Show};
|
||||
use tui::ui::Column;
|
||||
use tui::ui::{span, BufferedValue, Spacing};
|
||||
use tui::ui;
|
||||
use tui::ui::span;
|
||||
use tui::ui::widget::{
|
||||
ContainerState, TableState, TextEditState, TextViewState, TreeState, Window,
|
||||
};
|
||||
use tui::ui::{Borders, BufferedValue, Column, Show, Spacing, ToRow, Ui};
|
||||
use tui::{Channel, Exit};
|
||||
|
||||
use crate::cob::issue;
|
||||
@@ -476,7 +475,7 @@ impl store::Update<Message> for App {
|
||||
}
|
||||
|
||||
impl Show<Message> for App {
|
||||
fn show(&self, ctx: &im::Context<Message>, frame: &mut Frame) -> Result<()> {
|
||||
fn show(&self, ctx: &ui::Context<Message>, frame: &mut Frame) -> Result<()> {
|
||||
Window::default().show(ctx, |ui| {
|
||||
match self.state.page.clone() {
|
||||
state::Page::Main => {
|
||||
@@ -491,7 +490,7 @@ impl Show<Message> for App {
|
||||
{ (self.state.sections.focus(), self.state.sections.len()) };
|
||||
|
||||
let group = ui.container(
|
||||
im::Layout::Expandable3 {
|
||||
ui::Layout::Expandable3 {
|
||||
left_only: !self.state.preview.show,
|
||||
},
|
||||
&mut focus,
|
||||
@@ -586,7 +585,7 @@ impl Show<Message> for App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn show_browser(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_browser(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
let issues = self.issues.lock().unwrap();
|
||||
let issues = issues
|
||||
.iter()
|
||||
@@ -675,7 +674,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
let mut search = self.state.browser.search.clone();
|
||||
let (mut search_text, mut search_cursor) =
|
||||
(search.clone().read().text, search.clone().read().cursor);
|
||||
@@ -710,7 +709,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_browser_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
fn show_browser_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
use radicle::issue::{CloseReason, State};
|
||||
|
||||
let context = {
|
||||
@@ -809,7 +808,7 @@ impl App {
|
||||
ui.column_bar(frame, context, Spacing::from(0), Some(Borders::None));
|
||||
}
|
||||
|
||||
pub fn show_browser_shortcuts(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_browser_shortcuts(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
use radicle::issue::State;
|
||||
|
||||
let issues = self.issues.lock().unwrap();
|
||||
@@ -839,7 +838,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn show_issue(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_issue(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
#[derive(Clone)]
|
||||
struct Property<'a>(Span<'a>, Text<'a>);
|
||||
|
||||
@@ -997,7 +996,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn show_issue_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_issue_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
ui.column_bar(
|
||||
frame,
|
||||
[
|
||||
@@ -1018,7 +1017,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn show_issue_shortcuts(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_issue_shortcuts(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
let shortcuts = vec![("e", "edit"), ("c", "reply")];
|
||||
let global_shortcuts = vec![("p", "toggle preview"), ("?", "help")];
|
||||
|
||||
@@ -1032,7 +1031,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn show_comment(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_comment(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
let (text, footer, mut cursor) = {
|
||||
let comment = self.state.preview.selected_comment();
|
||||
let body: String = comment
|
||||
@@ -1065,7 +1064,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_comment_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_comment_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
ui.column_bar(
|
||||
frame,
|
||||
[
|
||||
@@ -1086,7 +1085,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn show_comment_shortcuts(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_comment_shortcuts(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
let shortcuts = vec![("e", "edit"), ("c", "reply")];
|
||||
let global_shortcuts = vec![("p", "toggle preview"), ("?", "help")];
|
||||
|
||||
@@ -1100,7 +1099,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
|
||||
fn show_help_text(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
fn show_help_text(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
ui.column_bar(
|
||||
frame,
|
||||
[Column::new(Span::raw(" Help ").bold(), Constraint::Fill(1))].to_vec(),
|
||||
@@ -1122,7 +1121,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_help_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
fn show_help_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
ui.column_bar(
|
||||
frame,
|
||||
[
|
||||
|
||||
+14
-15
@@ -3,6 +3,8 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use radicle::patch::cache::Patches;
|
||||
use radicle::patch::PatchId;
|
||||
use radicle::storage::git::Repository;
|
||||
@@ -15,15 +17,12 @@ use ratatui::{Frame, Viewport};
|
||||
|
||||
use radicle_tui as tui;
|
||||
|
||||
use serde::Serialize;
|
||||
use tui::event::Key;
|
||||
use tui::store;
|
||||
use tui::task::EmptyProcessors;
|
||||
use tui::ui::im;
|
||||
use tui::ui::im::widget::{ContainerState, TableState, TextEditState, TextViewState, Window};
|
||||
use tui::ui::im::Borders;
|
||||
use tui::ui::im::Show;
|
||||
use tui::ui::{BufferedValue, Column, Spacing};
|
||||
use tui::ui;
|
||||
use tui::ui::widget::{ContainerState, TableState, TextEditState, TextViewState, Window};
|
||||
use tui::ui::{Borders, BufferedValue, Column, Show, Spacing, Ui};
|
||||
use tui::{Channel, Exit};
|
||||
|
||||
use crate::ui::items::filter::Filter;
|
||||
@@ -238,7 +237,7 @@ impl store::Update<Message> for App {
|
||||
}
|
||||
|
||||
impl Show<Message> for App {
|
||||
fn show(&self, ctx: &im::Context<Message>, frame: &mut Frame) -> Result<()> {
|
||||
fn show(&self, ctx: &ui::Context<Message>, frame: &mut Frame) -> Result<()> {
|
||||
Window::default().show(ctx, |ui| {
|
||||
match self.state.page {
|
||||
Page::Main => {
|
||||
@@ -252,7 +251,7 @@ impl Show<Message> for App {
|
||||
let mut group_focus = self.state.main_group.focus();
|
||||
|
||||
let group = ui.container(
|
||||
im::Layout::Expandable3 { left_only: true },
|
||||
ui::Layout::Expandable3 { left_only: true },
|
||||
&mut group_focus,
|
||||
|ui| {
|
||||
self.show_browser(frame, ui);
|
||||
@@ -310,7 +309,7 @@ impl Show<Message> for App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn show_browser(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_browser(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
let patches = self.patches.lock().unwrap();
|
||||
let patches = patches
|
||||
.iter()
|
||||
@@ -378,14 +377,14 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_browser_footer(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
fn show_browser_footer(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
ui.layout(Layout::vertical([1, 1]), None, |ui| {
|
||||
self.show_browser_context(frame, ui);
|
||||
self.show_browser_shortcuts(frame, ui);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
let (mut search_text, mut search_cursor) = (
|
||||
self.state.search.clone().read().text,
|
||||
self.state.search.clone().read().cursor,
|
||||
@@ -416,7 +415,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_browser_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
fn show_browser_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
let context = {
|
||||
let patches = self.patches.lock().unwrap();
|
||||
let search = self.state.search.read().text;
|
||||
@@ -560,7 +559,7 @@ impl App {
|
||||
ui.column_bar(frame, context, Spacing::from(0), Some(Borders::None));
|
||||
}
|
||||
|
||||
pub fn show_browser_shortcuts(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
pub fn show_browser_shortcuts(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
ui.shortcuts(
|
||||
frame,
|
||||
&[
|
||||
@@ -575,7 +574,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
|
||||
fn show_help_text(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
fn show_help_text(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
ui.column_bar(
|
||||
frame,
|
||||
[Column::new(Span::raw(" Help ").bold(), Constraint::Fill(1))].to_vec(),
|
||||
@@ -597,7 +596,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_help_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
|
||||
fn show_help_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
|
||||
ui.column_bar(
|
||||
frame,
|
||||
[
|
||||
|
||||
@@ -23,9 +23,9 @@ use radicle_tui as tui;
|
||||
use tui::event::Key;
|
||||
use tui::store;
|
||||
use tui::task::EmptyProcessors;
|
||||
use tui::ui::im::widget::{ContainerState, TableState, TextViewState, Window};
|
||||
use tui::ui::im::{Borders, Context, Show, Ui};
|
||||
use tui::ui::span;
|
||||
use tui::ui::widget::{ContainerState, TableState, TextViewState, Window};
|
||||
use tui::ui::{Borders, Context, Show, Ui};
|
||||
use tui::ui::{Column, Spacing};
|
||||
use tui::{Channel, Exit};
|
||||
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
pub mod format;
|
||||
pub mod im;
|
||||
pub mod items;
|
||||
pub mod layout;
|
||||
pub mod span;
|
||||
|
||||
use radicle_tui::ui::Spacing;
|
||||
|
||||
use ratatui::layout::{Constraint, Layout};
|
||||
use ratatui::Frame;
|
||||
|
||||
use radicle_tui as tui;
|
||||
|
||||
use tui::event::Key;
|
||||
use tui::ui::widget::{TableState, TextEditState, Widget};
|
||||
use tui::ui::{Borders, Response, Ui};
|
||||
use tui::ui::{BufferedValue, Column, ToRow};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TerminalInfo {
|
||||
pub luma: Option<f32>,
|
||||
@@ -14,3 +25,180 @@ impl TerminalInfo {
|
||||
self.luma.unwrap_or_default() <= 0.6
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UiExt<'a, M>(&'a mut Ui<M>);
|
||||
|
||||
impl<'a, M> UiExt<'a, M> {
|
||||
pub fn new(ui: &'a mut Ui<M>) -> Self {
|
||||
Self(ui)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, M> From<&'a mut Ui<M>> for UiExt<'a, M> {
|
||||
fn from(ui: &'a mut Ui<M>) -> Self {
|
||||
Self::new(ui)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl<'a, M> UiExt<'a, M>
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn browser<R, const W: usize>(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
selected: &'a mut Option<usize>,
|
||||
items: &'a Vec<R>,
|
||||
header: impl IntoIterator<Item = Column<'a>>,
|
||||
footer: impl IntoIterator<Item = Column<'a>>,
|
||||
show_search: &'a mut bool,
|
||||
search: &'a mut BufferedValue<TextEditState>,
|
||||
) -> Response
|
||||
where
|
||||
R: ToRow<W> + Clone,
|
||||
{
|
||||
Browser::<R, W>::new(selected, items, header, footer, show_search, search).ui(self.0, frame)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BrowserState {
|
||||
items: TableState,
|
||||
search: BufferedValue<TextEditState>,
|
||||
show_search: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl BrowserState {
|
||||
pub fn new(items: TableState, search: BufferedValue<TextEditState>, show_search: bool) -> Self {
|
||||
Self {
|
||||
items,
|
||||
search,
|
||||
show_search,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.items.selected()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Browser<'a, R, const W: usize> {
|
||||
items: &'a Vec<R>,
|
||||
selected: &'a mut Option<usize>,
|
||||
header: Vec<Column<'a>>,
|
||||
footer: Vec<Column<'a>>,
|
||||
show_search: &'a mut bool,
|
||||
search: &'a mut BufferedValue<TextEditState>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl<'a, R, const W: usize> Browser<'a, R, W> {
|
||||
pub fn new(
|
||||
selected: &'a mut Option<usize>,
|
||||
items: &'a Vec<R>,
|
||||
header: impl IntoIterator<Item = Column<'a>>,
|
||||
footer: impl IntoIterator<Item = Column<'a>>,
|
||||
show_search: &'a mut bool,
|
||||
search: &'a mut BufferedValue<TextEditState>,
|
||||
) -> Self {
|
||||
Self {
|
||||
items,
|
||||
selected,
|
||||
header: header.into_iter().collect(),
|
||||
footer: footer.into_iter().collect(),
|
||||
show_search,
|
||||
search,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn items(&self) -> &Vec<R> {
|
||||
self.items
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO(erikli): Implement `show` that returns an `InnerResponse` such that it can
|
||||
/// used like a group.
|
||||
impl<R, const W: usize> Widget for Browser<'_, R, W>
|
||||
where
|
||||
R: ToRow<W> + Clone,
|
||||
{
|
||||
fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
let mut response = Response::default();
|
||||
let (mut text, mut cursor) = (self.search.read().text, self.search.read().cursor);
|
||||
|
||||
ui.layout(
|
||||
Layout::vertical([
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
Constraint::Length(if *self.show_search { 2 } else { 3 }),
|
||||
]),
|
||||
Some(1),
|
||||
|ui| {
|
||||
ui.column_bar(
|
||||
frame,
|
||||
self.header.clone().to_vec(),
|
||||
Spacing::default(),
|
||||
Some(Borders::Top),
|
||||
);
|
||||
|
||||
let table = ui.table(
|
||||
frame,
|
||||
self.selected,
|
||||
self.items,
|
||||
self.header.to_vec(),
|
||||
None,
|
||||
Spacing::from(1),
|
||||
if *self.show_search {
|
||||
Some(Borders::BottomSides)
|
||||
} else {
|
||||
Some(Borders::Sides)
|
||||
},
|
||||
);
|
||||
response.changed |= table.changed;
|
||||
|
||||
if *self.show_search {
|
||||
let text_edit = ui.text_edit_singleline(
|
||||
frame,
|
||||
&mut text,
|
||||
&mut cursor,
|
||||
Some("Search".to_string()),
|
||||
Some(Borders::Spacer { top: 0, left: 1 }),
|
||||
);
|
||||
self.search.write(TextEditState { text, cursor });
|
||||
response.changed |= text_edit.changed;
|
||||
} else {
|
||||
ui.column_bar(
|
||||
frame,
|
||||
self.footer.clone().to_vec(),
|
||||
Spacing::from(0),
|
||||
Some(Borders::Bottom),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if !*self.show_search {
|
||||
if ui.has_input(|key| key == Key::Char('/')) {
|
||||
*self.show_search = true;
|
||||
}
|
||||
} else {
|
||||
if ui.has_input(|key| key == Key::Esc) {
|
||||
*self.show_search = false;
|
||||
self.search.reset();
|
||||
}
|
||||
if ui.has_input(|key| key == Key::Enter) {
|
||||
*self.show_search = false;
|
||||
self.search.apply();
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
-188
@@ -1,188 +0,0 @@
|
||||
use radicle_tui::ui::Spacing;
|
||||
|
||||
use ratatui::layout::{Constraint, Layout};
|
||||
use ratatui::Frame;
|
||||
|
||||
use radicle_tui as tui;
|
||||
|
||||
use tui::event::Key;
|
||||
use tui::ui::im::widget::{TableState, TextEditState, Widget};
|
||||
use tui::ui::im::{Borders, Response, Ui};
|
||||
use tui::ui::{BufferedValue, Column, ToRow};
|
||||
|
||||
pub struct UiExt<'a, M>(&'a mut Ui<M>);
|
||||
|
||||
impl<'a, M> UiExt<'a, M> {
|
||||
pub fn new(ui: &'a mut Ui<M>) -> Self {
|
||||
Self(ui)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, M> From<&'a mut Ui<M>> for UiExt<'a, M> {
|
||||
fn from(ui: &'a mut Ui<M>) -> Self {
|
||||
Self::new(ui)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl<'a, M> UiExt<'a, M>
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn browser<R, const W: usize>(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
selected: &'a mut Option<usize>,
|
||||
items: &'a Vec<R>,
|
||||
header: impl IntoIterator<Item = Column<'a>>,
|
||||
footer: impl IntoIterator<Item = Column<'a>>,
|
||||
show_search: &'a mut bool,
|
||||
search: &'a mut BufferedValue<TextEditState>,
|
||||
) -> Response
|
||||
where
|
||||
R: ToRow<W> + Clone,
|
||||
{
|
||||
Browser::<R, W>::new(selected, items, header, footer, show_search, search).ui(self.0, frame)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BrowserState {
|
||||
items: TableState,
|
||||
search: BufferedValue<TextEditState>,
|
||||
show_search: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl BrowserState {
|
||||
pub fn new(items: TableState, search: BufferedValue<TextEditState>, show_search: bool) -> Self {
|
||||
Self {
|
||||
items,
|
||||
search,
|
||||
show_search,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.items.selected()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Browser<'a, R, const W: usize> {
|
||||
items: &'a Vec<R>,
|
||||
selected: &'a mut Option<usize>,
|
||||
header: Vec<Column<'a>>,
|
||||
footer: Vec<Column<'a>>,
|
||||
show_search: &'a mut bool,
|
||||
search: &'a mut BufferedValue<TextEditState>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl<'a, R, const W: usize> Browser<'a, R, W> {
|
||||
pub fn new(
|
||||
selected: &'a mut Option<usize>,
|
||||
items: &'a Vec<R>,
|
||||
header: impl IntoIterator<Item = Column<'a>>,
|
||||
footer: impl IntoIterator<Item = Column<'a>>,
|
||||
show_search: &'a mut bool,
|
||||
search: &'a mut BufferedValue<TextEditState>,
|
||||
) -> Self {
|
||||
Self {
|
||||
items,
|
||||
selected,
|
||||
header: header.into_iter().collect(),
|
||||
footer: footer.into_iter().collect(),
|
||||
show_search,
|
||||
search,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn items(&self) -> &Vec<R> {
|
||||
self.items
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO(erikli): Implement `show` that returns an `InnerResponse` such that it can
|
||||
/// used like a group.
|
||||
impl<R, const W: usize> Widget for Browser<'_, R, W>
|
||||
where
|
||||
R: ToRow<W> + Clone,
|
||||
{
|
||||
fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
let mut response = Response::default();
|
||||
let (mut text, mut cursor) = (self.search.read().text, self.search.read().cursor);
|
||||
|
||||
ui.layout(
|
||||
Layout::vertical([
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
Constraint::Length(if *self.show_search { 2 } else { 3 }),
|
||||
]),
|
||||
Some(1),
|
||||
|ui| {
|
||||
ui.column_bar(
|
||||
frame,
|
||||
self.header.clone().to_vec(),
|
||||
Spacing::default(),
|
||||
Some(Borders::Top),
|
||||
);
|
||||
|
||||
let table = ui.table(
|
||||
frame,
|
||||
self.selected,
|
||||
self.items,
|
||||
self.header.to_vec(),
|
||||
None,
|
||||
Spacing::from(1),
|
||||
if *self.show_search {
|
||||
Some(Borders::BottomSides)
|
||||
} else {
|
||||
Some(Borders::Sides)
|
||||
},
|
||||
);
|
||||
response.changed |= table.changed;
|
||||
|
||||
if *self.show_search {
|
||||
let text_edit = ui.text_edit_singleline(
|
||||
frame,
|
||||
&mut text,
|
||||
&mut cursor,
|
||||
Some("Search".to_string()),
|
||||
Some(Borders::Spacer { top: 0, left: 1 }),
|
||||
);
|
||||
self.search.write(TextEditState { text, cursor });
|
||||
response.changed |= text_edit.changed;
|
||||
} else {
|
||||
ui.column_bar(
|
||||
frame,
|
||||
self.footer.clone().to_vec(),
|
||||
Spacing::from(0),
|
||||
Some(Borders::Bottom),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if !*self.show_search {
|
||||
if ui.has_input(|key| key == Key::Char('/')) {
|
||||
*self.show_search = true;
|
||||
}
|
||||
} else {
|
||||
if ui.has_input(|key| key == Key::Esc) {
|
||||
*self.show_search = false;
|
||||
self.search.reset();
|
||||
}
|
||||
if ui.has_input(|key| key == Key::Enter) {
|
||||
*self.show_search = false;
|
||||
self.search.apply();
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -9,9 +9,8 @@ use radicle_tui as tui;
|
||||
use tui::event::Key;
|
||||
use tui::store;
|
||||
use tui::task::EmptyProcessors;
|
||||
use tui::ui::im::widget::Window;
|
||||
use tui::ui::im::Show;
|
||||
use tui::ui::im::{Borders, Context};
|
||||
use tui::ui::widget::Window;
|
||||
use tui::ui::{Borders, Context, Show};
|
||||
use tui::{Channel, Exit};
|
||||
|
||||
const ALIEN: &str = r#"
|
||||
|
||||
@@ -11,17 +11,12 @@ use ratatui::{Frame, Viewport};
|
||||
use radicle_tui as tui;
|
||||
|
||||
use tui::event::Key;
|
||||
use tui::store::Update;
|
||||
use tui::task::EmptyProcessors;
|
||||
use tui::ui::im::widget::Window;
|
||||
use tui::ui::im::{Borders, Context};
|
||||
use tui::ui::Spacing;
|
||||
use tui::ui::{Column, ToRow};
|
||||
use tui::ui::widget::{TableState, Window};
|
||||
use tui::ui::{Borders, Column, Context, Show, Spacing, ToRow};
|
||||
use tui::Channel;
|
||||
use tui::{
|
||||
store::Update,
|
||||
ui::im::{widget::TableState, Show},
|
||||
Exit,
|
||||
};
|
||||
use tui::Exit;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Item {
|
||||
|
||||
+2
-3
@@ -20,8 +20,7 @@ use ratatui::Viewport;
|
||||
|
||||
use store::Update;
|
||||
use terminal::StdinReader;
|
||||
use ui::im;
|
||||
use ui::im::Show;
|
||||
use ui::{Frontend, Show};
|
||||
|
||||
use crate::task::Process;
|
||||
|
||||
@@ -176,7 +175,7 @@ where
|
||||
|
||||
let store = store::Store::<S, M, R>::new(state_tx.clone());
|
||||
let worker = task::Worker::<T, M, R>::new(work_tx.clone());
|
||||
let frontend = im::Frontend::default();
|
||||
let frontend = Frontend::default();
|
||||
let stdin_reader = StdinReader::default();
|
||||
|
||||
// TODO(erikli): Handle errors
|
||||
|
||||
@@ -1,21 +1,674 @@
|
||||
pub mod ext;
|
||||
pub mod im;
|
||||
pub mod layout;
|
||||
pub mod span;
|
||||
pub mod theme;
|
||||
pub mod utils;
|
||||
pub mod widget;
|
||||
|
||||
use ratatui::layout::Constraint;
|
||||
use ratatui::text::Text;
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use std::hash::Hash;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use ratatui::layout::{Alignment, Constraint, Flex, Position, Rect};
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::text::{Span, Text};
|
||||
use ratatui::widgets::Cell;
|
||||
use ratatui::{Frame, Viewport};
|
||||
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
|
||||
use tui_tree_widget::TreeItem;
|
||||
|
||||
use crate::event::{Event, Key};
|
||||
use crate::store::Update;
|
||||
use crate::terminal::Terminal;
|
||||
use crate::ui::theme::Theme;
|
||||
use crate::ui::widget::{AddContentFn, Widget};
|
||||
use crate::{Interrupted, Share};
|
||||
|
||||
pub const RENDER_WIDTH_XSMALL: usize = 50;
|
||||
pub const RENDER_WIDTH_SMALL: usize = 70;
|
||||
pub const RENDER_WIDTH_MEDIUM: usize = 150;
|
||||
pub const RENDER_WIDTH_LARGE: usize = usize::MAX;
|
||||
|
||||
const RENDERING_TICK_RATE: Duration = Duration::from_millis(250);
|
||||
|
||||
/// The main UI trait for the ability to render an application.
|
||||
pub trait Show<M> {
|
||||
fn show(&self, ctx: &Context<M>, frame: &mut Frame) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Frontend {}
|
||||
|
||||
impl Frontend {
|
||||
pub async fn run<S, M, R>(
|
||||
self,
|
||||
message_tx: broadcast::Sender<M>,
|
||||
mut state_rx: UnboundedReceiver<S>,
|
||||
mut event_rx: UnboundedReceiver<Event>,
|
||||
mut interrupt_rx: broadcast::Receiver<Interrupted<R>>,
|
||||
viewport: Viewport,
|
||||
) -> anyhow::Result<Interrupted<R>>
|
||||
where
|
||||
S: Update<M, Return = R> + Show<M>,
|
||||
M: Share,
|
||||
R: Share,
|
||||
{
|
||||
let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);
|
||||
let mut terminal = Terminal::try_from(viewport)?;
|
||||
|
||||
let mut state = state_rx.recv().await.unwrap();
|
||||
let mut ctx = Context::default().with_sender(message_tx);
|
||||
|
||||
let result: anyhow::Result<Interrupted<R>> = loop {
|
||||
tokio::select! {
|
||||
// Tick to terminate the select every N milliseconds
|
||||
_ = ticker.tick() => (),
|
||||
// Handle input events
|
||||
Some(event) = event_rx.recv() => {
|
||||
match event {
|
||||
Event::Key(key) => {
|
||||
log::debug!(target: "frontend", "Received key event: {key:?}");
|
||||
ctx.store_input(event)
|
||||
}
|
||||
Event::Resize(x, y) => {
|
||||
log::debug!(target: "frontend", "Received resize event: {x},{y}");
|
||||
terminal.clear()?;
|
||||
},
|
||||
Event::Unknown => {
|
||||
log::debug!(target: "frontend", "Received unknown event")
|
||||
}
|
||||
}
|
||||
},
|
||||
// Handle state updates
|
||||
Some(s) = state_rx.recv() => {
|
||||
state = s;
|
||||
},
|
||||
// Catch and handle interrupt signal to gracefully shutdown
|
||||
Ok(interrupted) = interrupt_rx.recv() => {
|
||||
break Ok(interrupted);
|
||||
}
|
||||
}
|
||||
terminal.draw(|frame| {
|
||||
let ctx = ctx.clone().with_frame_size(frame.area());
|
||||
|
||||
if let Err(err) = state.show(&ctx, frame) {
|
||||
log::warn!("Drawing failed: {err}");
|
||||
}
|
||||
})?;
|
||||
|
||||
ctx.clear_inputs();
|
||||
};
|
||||
terminal.restore()?;
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Response {
|
||||
pub changed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InnerResponse<R> {
|
||||
/// What the user closure returned.
|
||||
pub inner: R,
|
||||
/// The response of the area.
|
||||
pub response: Response,
|
||||
}
|
||||
|
||||
impl<R> InnerResponse<R> {
|
||||
#[inline]
|
||||
pub fn new(inner: R, response: Response) -> Self {
|
||||
Self { inner, response }
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Context` is held by the `Ui` and reflects the environment a `Ui` runs in.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Context<M> {
|
||||
/// Currently captured user inputs. Inputs that where stored via `store_input`
|
||||
/// need to be cleared manually via `clear_inputs` (usually for each frame drawn).
|
||||
inputs: VecDeque<Event>,
|
||||
/// Current frame of the application.
|
||||
pub(crate) frame_size: Rect,
|
||||
/// The message sender used by the `Ui` to send application messages.
|
||||
pub(crate) sender: Option<broadcast::Sender<M>>,
|
||||
}
|
||||
|
||||
impl<M> Default for Context<M> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inputs: VecDeque::default(),
|
||||
frame_size: Rect::default(),
|
||||
sender: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Context<M> {
|
||||
pub fn new(frame_size: Rect) -> Self {
|
||||
Self {
|
||||
frame_size,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_inputs(mut self, inputs: VecDeque<Event>) -> Self {
|
||||
self.inputs = inputs;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_frame_size(mut self, frame_size: Rect) -> Self {
|
||||
self.frame_size = frame_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_sender(mut self, sender: broadcast::Sender<M>) -> Self {
|
||||
self.sender = Some(sender);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn frame_size(&self) -> Rect {
|
||||
self.frame_size
|
||||
}
|
||||
|
||||
pub fn store_input(&mut self, event: Event) {
|
||||
self.inputs.push_back(event);
|
||||
}
|
||||
|
||||
pub fn clear_inputs(&mut self) {
|
||||
self.inputs.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// `Borders` defines which borders should be drawn around a widget.
|
||||
pub enum Borders {
|
||||
None,
|
||||
Spacer { top: usize, left: usize },
|
||||
All,
|
||||
Top,
|
||||
Sides,
|
||||
Bottom,
|
||||
BottomSides,
|
||||
}
|
||||
|
||||
/// A `Layout` is used to support pre-defined layouts. It either represents
|
||||
/// such a predefined layout or a wrapped `ratatui` layout. It's used internally
|
||||
/// but can be build from a `ratatui` layout.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub enum Layout {
|
||||
#[default]
|
||||
None,
|
||||
Wrapped {
|
||||
internal: ratatui::layout::Layout,
|
||||
},
|
||||
Expandable3 {
|
||||
left_only: bool,
|
||||
},
|
||||
Popup {
|
||||
percent_x: u16,
|
||||
percent_y: u16,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ratatui::layout::Layout> for Layout {
|
||||
fn from(layout: ratatui::layout::Layout) -> Self {
|
||||
Layout::Wrapped { internal: layout }
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
Layout::None => 0,
|
||||
Layout::Wrapped { internal } => internal.split(Rect::default()).len(),
|
||||
Layout::Expandable3 { left_only } => {
|
||||
if *left_only {
|
||||
1
|
||||
} else {
|
||||
3
|
||||
}
|
||||
}
|
||||
Layout::Popup {
|
||||
percent_x: _,
|
||||
percent_y: _,
|
||||
} => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn split(&self, area: Rect) -> Rc<[Rect]> {
|
||||
match self {
|
||||
Layout::None => Rc::new([]),
|
||||
Layout::Wrapped { internal } => internal.split(area),
|
||||
Layout::Expandable3 { left_only } => {
|
||||
use ratatui::layout::Layout;
|
||||
|
||||
if *left_only {
|
||||
[area].into()
|
||||
} else if area.width <= 140 {
|
||||
let [left, right] = Layout::horizontal([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
])
|
||||
.areas(area);
|
||||
let [right_top, right_bottom] =
|
||||
Layout::vertical([Constraint::Percentage(65), Constraint::Percentage(35)])
|
||||
.areas(right);
|
||||
|
||||
[left, right_top, right_bottom].into()
|
||||
} else {
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
])
|
||||
.split(area)
|
||||
}
|
||||
}
|
||||
Layout::Popup {
|
||||
percent_x,
|
||||
percent_y,
|
||||
} => {
|
||||
use ratatui::layout::Layout;
|
||||
|
||||
let vertical =
|
||||
Layout::vertical([Constraint::Percentage(*percent_y)]).flex(Flex::Center);
|
||||
let horizontal =
|
||||
Layout::horizontal([Constraint::Percentage(*percent_x)]).flex(Flex::Center);
|
||||
let [area] = vertical.areas(area);
|
||||
let [area] = horizontal.areas(area);
|
||||
|
||||
[area].into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Ui` is the main frontend component that provides render and user-input capture
|
||||
/// capabilities. An application consists of at least 1 root `Ui`. An `Ui` can build child
|
||||
/// `Ui`s that partially inherit attributes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Ui<M> {
|
||||
/// The context this runs in: frame sizes, captured user-inputs etc.
|
||||
ctx: Context<M>,
|
||||
/// The UI theme.
|
||||
theme: Theme,
|
||||
/// The area this can render in.
|
||||
area: Rect,
|
||||
/// The layout used to calculate the next area to draw.
|
||||
layout: Layout,
|
||||
/// Currently focused area.
|
||||
focus_area: Option<usize>,
|
||||
/// If this has focus.
|
||||
has_focus: bool,
|
||||
/// Current rendering counter that is increased whenever the next area to draw
|
||||
/// on is requested.
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl<M> Ui<M> {
|
||||
pub fn has_input(&mut self, f: impl Fn(Key) -> bool) -> bool {
|
||||
self.has_focus
|
||||
&& self.is_area_focused()
|
||||
&& self.ctx.inputs.iter().any(|event| {
|
||||
if let Event::Key(key) = event {
|
||||
return f(*key);
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_global_input(&mut self, f: impl Fn(Key) -> bool) -> bool {
|
||||
self.has_focus
|
||||
&& self.ctx.inputs.iter().any(|event| {
|
||||
if let Event::Key(key) = event {
|
||||
return f(*key);
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_input(&mut self, f: impl Fn(Key) -> bool) -> Option<Key> {
|
||||
if self.has_focus && self.is_area_focused() {
|
||||
let matches = |&event| {
|
||||
if let Event::Key(key) = event {
|
||||
return f(key);
|
||||
}
|
||||
false
|
||||
};
|
||||
|
||||
if let Some(Event::Key(key)) =
|
||||
self.ctx.inputs.iter().find(|event| matches(event)).copied()
|
||||
{
|
||||
return Some(key);
|
||||
}
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Default for Ui<M> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: Theme::default(),
|
||||
area: Rect::default(),
|
||||
layout: Layout::default(),
|
||||
focus_area: None,
|
||||
has_focus: true,
|
||||
count: 0,
|
||||
ctx: Context::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Ui<M> {
|
||||
pub fn new(area: Rect) -> Self {
|
||||
Self {
|
||||
area,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_area(mut self, area: Rect) -> Self {
|
||||
self.area = area;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_layout(mut self, layout: Layout) -> Self {
|
||||
self.layout = layout;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_area_focus(mut self, focus: Option<usize>) -> Self {
|
||||
self.focus_area = focus;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_ctx(mut self, ctx: Context<M>) -> Self {
|
||||
self.ctx = ctx;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_focus(mut self) -> Self {
|
||||
self.has_focus = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn without_focus(mut self) -> Self {
|
||||
self.has_focus = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn theme(&self) -> &Theme {
|
||||
&self.theme
|
||||
}
|
||||
|
||||
pub fn area(&self) -> Rect {
|
||||
self.area
|
||||
}
|
||||
|
||||
pub fn next_area(&mut self) -> Option<(Rect, bool)> {
|
||||
let area_focus = self
|
||||
.focus_area
|
||||
.map(|focus| self.count == focus)
|
||||
.unwrap_or(false);
|
||||
let rect = self.layout.split(self.area).get(self.count).cloned();
|
||||
|
||||
self.count += 1;
|
||||
|
||||
rect.map(|rect| (rect, area_focus))
|
||||
}
|
||||
|
||||
pub fn current_area(&mut self) -> Option<(Rect, bool)> {
|
||||
let count = self.count.saturating_sub(1);
|
||||
|
||||
let area_focus = self.focus_area.map(|focus| count == focus).unwrap_or(false);
|
||||
let rect = self.layout.split(self.area).get(self.count).cloned();
|
||||
|
||||
rect.map(|rect| (rect, area_focus))
|
||||
}
|
||||
|
||||
pub fn is_area_focused(&self) -> bool {
|
||||
let count = self.count.saturating_sub(1);
|
||||
self.focus_area.map(|focus| count == focus).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
|
||||
pub fn focus_next(&mut self) {
|
||||
if self.focus_area.is_none() {
|
||||
self.focus_area = Some(0);
|
||||
} else {
|
||||
self.focus_area = Some(self.focus_area.unwrap().saturating_add(1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_message(&self, message: M) {
|
||||
if let Some(sender) = &self.ctx.sender {
|
||||
let _ = sender.send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Ui<M>
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
pub fn add(&mut self, frame: &mut Frame, widget: impl Widget) -> Response {
|
||||
widget.ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn child_ui(&mut self, area: Rect, layout: impl Into<Layout>) -> Self {
|
||||
Ui::default()
|
||||
.with_area(area)
|
||||
.with_layout(layout.into())
|
||||
.with_ctx(self.ctx.clone())
|
||||
}
|
||||
|
||||
pub fn layout<R>(
|
||||
&mut self,
|
||||
layout: impl Into<Layout>,
|
||||
focus: Option<usize>,
|
||||
add_contents: impl FnOnce(&mut Self) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.layout_dyn(layout, focus, Box::new(add_contents))
|
||||
}
|
||||
|
||||
pub fn layout_dyn<R>(
|
||||
&mut self,
|
||||
layout: impl Into<Layout>,
|
||||
focus: Option<usize>,
|
||||
add_contents: Box<AddContentFn<M, R>>,
|
||||
) -> InnerResponse<R> {
|
||||
let (area, area_focus) = self.next_area().unwrap_or_default();
|
||||
|
||||
let mut child_ui = Ui {
|
||||
has_focus: area_focus,
|
||||
focus_area: focus,
|
||||
..self.child_ui(area, layout)
|
||||
};
|
||||
|
||||
InnerResponse::new(add_contents(&mut child_ui), Response::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Ui<M>
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
pub fn container<R>(
|
||||
&mut self,
|
||||
layout: impl Into<Layout>,
|
||||
focus: &mut Option<usize>,
|
||||
add_contents: impl FnOnce(&mut Ui<M>) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
let (area, area_focus) = self.next_area().unwrap_or_default();
|
||||
|
||||
let layout: Layout = layout.into();
|
||||
let len = layout.len();
|
||||
|
||||
// TODO(erikli): Check if setting the focus area is needed at all.
|
||||
let mut child_ui = Ui {
|
||||
has_focus: area_focus,
|
||||
focus_area: *focus,
|
||||
..self.child_ui(area, layout)
|
||||
};
|
||||
|
||||
widget::Container::new(len, focus).show(&mut child_ui, add_contents)
|
||||
}
|
||||
|
||||
pub fn popup<R>(
|
||||
&mut self,
|
||||
layout: impl Into<Layout>,
|
||||
add_contents: impl FnOnce(&mut Ui<M>) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
let layout: Layout = layout.into();
|
||||
let areas = layout.split(self.area());
|
||||
let area = areas.first().cloned().unwrap_or(self.area());
|
||||
|
||||
let mut child_ui = self.child_ui(area, layout::fill());
|
||||
child_ui.has_focus = true;
|
||||
|
||||
widget::Popup::default().show(&mut child_ui, add_contents)
|
||||
}
|
||||
|
||||
pub fn label<'a>(&mut self, frame: &mut Frame, content: impl Into<Text<'a>>) -> Response {
|
||||
widget::Label::new(content).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn overline(&mut self, frame: &mut Frame) -> Response {
|
||||
let overline = String::from("▔").repeat(256);
|
||||
self.label(frame, Span::raw(overline).cyan())
|
||||
}
|
||||
|
||||
pub fn separator(&mut self, frame: &mut Frame) -> Response {
|
||||
let overline = String::from("─").repeat(256);
|
||||
self.label(
|
||||
frame,
|
||||
Span::raw(overline).fg(self.theme.border_style.fg.unwrap_or_default()),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn table<'a, R, const W: usize>(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
selected: &mut Option<usize>,
|
||||
items: &'a Vec<R>,
|
||||
columns: Vec<Column<'a>>,
|
||||
empty_message: Option<String>,
|
||||
spacing: Spacing,
|
||||
borders: Option<Borders>,
|
||||
) -> Response
|
||||
where
|
||||
R: ToRow<W> + Clone,
|
||||
{
|
||||
widget::Table::new(selected, items, columns, empty_message, borders)
|
||||
.spacing(spacing)
|
||||
.ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn tree<R, Id>(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
items: &'_ Vec<R>,
|
||||
opened: &mut Option<HashSet<Vec<Id>>>,
|
||||
selected: &mut Option<Vec<Id>>,
|
||||
borders: Option<Borders>,
|
||||
) -> Response
|
||||
where
|
||||
R: ToTree<Id> + Clone,
|
||||
Id: ToString + Clone + Eq + Hash,
|
||||
{
|
||||
widget::Tree::new(items, opened, selected, borders, false).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn shortcuts(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
shortcuts: &[(&str, &str)],
|
||||
divider: char,
|
||||
alignment: Alignment,
|
||||
) -> Response {
|
||||
widget::Shortcuts::new(shortcuts, divider, alignment).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn column_bar(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
columns: Vec<Column<'_>>,
|
||||
spacing: Spacing,
|
||||
borders: Option<Borders>,
|
||||
) -> Response {
|
||||
widget::ColumnBar::new(columns, spacing, borders).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn text_view<'a>(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
text: impl Into<Text<'a>>,
|
||||
scroll: &'a mut Position,
|
||||
borders: Option<Borders>,
|
||||
) -> Response {
|
||||
widget::TextView::new(text, None::<String>, scroll, borders).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn text_view_with_footer<'a>(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
text: impl Into<Text<'a>>,
|
||||
footer: impl Into<Text<'a>>,
|
||||
scroll: &'a mut Position,
|
||||
borders: Option<Borders>,
|
||||
) -> Response {
|
||||
widget::TextView::new(text, Some(footer), scroll, borders).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn centered_text_view<'a>(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
text: impl Into<Text<'a>>,
|
||||
borders: Option<Borders>,
|
||||
) -> Response {
|
||||
widget::CenteredTextView::new(text, borders).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn text_edit_singleline(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
text: &mut String,
|
||||
cursor: &mut usize,
|
||||
label: Option<impl ToString>,
|
||||
borders: Option<Borders>,
|
||||
) -> Response {
|
||||
match label {
|
||||
Some(label) => widget::TextEdit::new(text, cursor, borders)
|
||||
.with_label(label)
|
||||
.ui(self, frame),
|
||||
_ => widget::TextEdit::new(text, cursor, borders).ui(self, frame),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ColumnView {
|
||||
small: bool,
|
||||
|
||||
-664
@@ -1,664 +0,0 @@
|
||||
pub mod widget;
|
||||
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use std::fmt::Debug;
|
||||
use std::hash::Hash;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::{Span, Text};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
|
||||
use ratatui::layout::{Alignment, Constraint, Flex, Position, Rect};
|
||||
use ratatui::{Frame, Viewport};
|
||||
|
||||
use crate::event::{Event, Key};
|
||||
use crate::store::Update;
|
||||
use crate::terminal::Terminal;
|
||||
use crate::ui::theme::Theme;
|
||||
use crate::ui::{Column, Spacing, ToRow, ToTree};
|
||||
use crate::{Interrupted, Share};
|
||||
|
||||
use crate::ui::im::widget::Widget;
|
||||
|
||||
use self::widget::AddContentFn;
|
||||
|
||||
use super::layout;
|
||||
|
||||
const RENDERING_TICK_RATE: Duration = Duration::from_millis(250);
|
||||
|
||||
/// The main UI trait for the ability to render an application.
|
||||
pub trait Show<M> {
|
||||
fn show(&self, ctx: &Context<M>, frame: &mut Frame) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Frontend {}
|
||||
|
||||
impl Frontend {
|
||||
pub async fn run<S, M, R>(
|
||||
self,
|
||||
message_tx: broadcast::Sender<M>,
|
||||
mut state_rx: UnboundedReceiver<S>,
|
||||
mut event_rx: UnboundedReceiver<Event>,
|
||||
mut interrupt_rx: broadcast::Receiver<Interrupted<R>>,
|
||||
viewport: Viewport,
|
||||
) -> anyhow::Result<Interrupted<R>>
|
||||
where
|
||||
S: Update<M, Return = R> + Show<M>,
|
||||
M: Share,
|
||||
R: Share,
|
||||
{
|
||||
let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);
|
||||
let mut terminal = Terminal::try_from(viewport)?;
|
||||
|
||||
let mut state = state_rx.recv().await.unwrap();
|
||||
let mut ctx = Context::default().with_sender(message_tx);
|
||||
|
||||
let result: anyhow::Result<Interrupted<R>> = loop {
|
||||
tokio::select! {
|
||||
// Tick to terminate the select every N milliseconds
|
||||
_ = ticker.tick() => (),
|
||||
// Handle input events
|
||||
Some(event) = event_rx.recv() => {
|
||||
match event {
|
||||
Event::Key(key) => {
|
||||
log::debug!(target: "frontend", "Received key event: {key:?}");
|
||||
ctx.store_input(event)
|
||||
}
|
||||
Event::Resize(x, y) => {
|
||||
log::debug!(target: "frontend", "Received resize event: {x},{y}");
|
||||
terminal.clear()?;
|
||||
},
|
||||
Event::Unknown => {
|
||||
log::debug!(target: "frontend", "Received unknown event")
|
||||
}
|
||||
}
|
||||
},
|
||||
// Handle state updates
|
||||
Some(s) = state_rx.recv() => {
|
||||
state = s;
|
||||
},
|
||||
// Catch and handle interrupt signal to gracefully shutdown
|
||||
Ok(interrupted) = interrupt_rx.recv() => {
|
||||
break Ok(interrupted);
|
||||
}
|
||||
}
|
||||
terminal.draw(|frame| {
|
||||
let ctx = ctx.clone().with_frame_size(frame.area());
|
||||
|
||||
if let Err(err) = state.show(&ctx, frame) {
|
||||
log::warn!("Drawing failed: {err}");
|
||||
}
|
||||
})?;
|
||||
|
||||
ctx.clear_inputs();
|
||||
};
|
||||
terminal.restore()?;
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Response {
|
||||
pub changed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InnerResponse<R> {
|
||||
/// What the user closure returned.
|
||||
pub inner: R,
|
||||
/// The response of the area.
|
||||
pub response: Response,
|
||||
}
|
||||
|
||||
impl<R> InnerResponse<R> {
|
||||
#[inline]
|
||||
pub fn new(inner: R, response: Response) -> Self {
|
||||
Self { inner, response }
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Context` is held by the `Ui` and reflects the environment a `Ui` runs in.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Context<M> {
|
||||
/// Currently captured user inputs. Inputs that where stored via `store_input`
|
||||
/// need to be cleared manually via `clear_inputs` (usually for each frame drawn).
|
||||
inputs: VecDeque<Event>,
|
||||
/// Current frame of the application.
|
||||
pub(crate) frame_size: Rect,
|
||||
/// The message sender used by the `Ui` to send application messages.
|
||||
pub(crate) sender: Option<broadcast::Sender<M>>,
|
||||
}
|
||||
|
||||
impl<M> Default for Context<M> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inputs: VecDeque::default(),
|
||||
frame_size: Rect::default(),
|
||||
sender: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Context<M> {
|
||||
pub fn new(frame_size: Rect) -> Self {
|
||||
Self {
|
||||
frame_size,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_inputs(mut self, inputs: VecDeque<Event>) -> Self {
|
||||
self.inputs = inputs;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_frame_size(mut self, frame_size: Rect) -> Self {
|
||||
self.frame_size = frame_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_sender(mut self, sender: broadcast::Sender<M>) -> Self {
|
||||
self.sender = Some(sender);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn frame_size(&self) -> Rect {
|
||||
self.frame_size
|
||||
}
|
||||
|
||||
pub fn store_input(&mut self, event: Event) {
|
||||
self.inputs.push_back(event);
|
||||
}
|
||||
|
||||
pub fn clear_inputs(&mut self) {
|
||||
self.inputs.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// `Borders` defines which borders should be drawn around a widget.
|
||||
pub enum Borders {
|
||||
None,
|
||||
Spacer { top: usize, left: usize },
|
||||
All,
|
||||
Top,
|
||||
Sides,
|
||||
Bottom,
|
||||
BottomSides,
|
||||
}
|
||||
|
||||
/// A `Layout` is used to support pre-defined layouts. It either represents
|
||||
/// such a predefined layout or a wrapped `ratatui` layout. It's used internally
|
||||
/// but can be build from a `ratatui` layout.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub enum Layout {
|
||||
#[default]
|
||||
None,
|
||||
Wrapped {
|
||||
internal: ratatui::layout::Layout,
|
||||
},
|
||||
Expandable3 {
|
||||
left_only: bool,
|
||||
},
|
||||
Popup {
|
||||
percent_x: u16,
|
||||
percent_y: u16,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ratatui::layout::Layout> for Layout {
|
||||
fn from(layout: ratatui::layout::Layout) -> Self {
|
||||
Layout::Wrapped { internal: layout }
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
Layout::None => 0,
|
||||
Layout::Wrapped { internal } => internal.split(Rect::default()).len(),
|
||||
Layout::Expandable3 { left_only } => {
|
||||
if *left_only {
|
||||
1
|
||||
} else {
|
||||
3
|
||||
}
|
||||
}
|
||||
Layout::Popup {
|
||||
percent_x: _,
|
||||
percent_y: _,
|
||||
} => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn split(&self, area: Rect) -> Rc<[Rect]> {
|
||||
match self {
|
||||
Layout::None => Rc::new([]),
|
||||
Layout::Wrapped { internal } => internal.split(area),
|
||||
Layout::Expandable3 { left_only } => {
|
||||
use ratatui::layout::Layout;
|
||||
|
||||
if *left_only {
|
||||
[area].into()
|
||||
} else if area.width <= 140 {
|
||||
let [left, right] = Layout::horizontal([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
])
|
||||
.areas(area);
|
||||
let [right_top, right_bottom] =
|
||||
Layout::vertical([Constraint::Percentage(65), Constraint::Percentage(35)])
|
||||
.areas(right);
|
||||
|
||||
[left, right_top, right_bottom].into()
|
||||
} else {
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
])
|
||||
.split(area)
|
||||
}
|
||||
}
|
||||
Layout::Popup {
|
||||
percent_x,
|
||||
percent_y,
|
||||
} => {
|
||||
use ratatui::layout::Layout;
|
||||
|
||||
let vertical =
|
||||
Layout::vertical([Constraint::Percentage(*percent_y)]).flex(Flex::Center);
|
||||
let horizontal =
|
||||
Layout::horizontal([Constraint::Percentage(*percent_x)]).flex(Flex::Center);
|
||||
let [area] = vertical.areas(area);
|
||||
let [area] = horizontal.areas(area);
|
||||
|
||||
[area].into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Ui` is the main frontend component that provides render and user-input capture
|
||||
/// capabilities. An application consists of at least 1 root `Ui`. An `Ui` can build child
|
||||
/// `Ui`s that partially inherit attributes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Ui<M> {
|
||||
/// The context this runs in: frame sizes, captured user-inputs etc.
|
||||
ctx: Context<M>,
|
||||
/// The UI theme.
|
||||
theme: Theme,
|
||||
/// The area this can render in.
|
||||
area: Rect,
|
||||
/// The layout used to calculate the next area to draw.
|
||||
layout: Layout,
|
||||
/// Currently focused area.
|
||||
focus_area: Option<usize>,
|
||||
/// If this has focus.
|
||||
has_focus: bool,
|
||||
/// Current rendering counter that is increased whenever the next area to draw
|
||||
/// on is requested.
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl<M> Ui<M> {
|
||||
pub fn has_input(&mut self, f: impl Fn(Key) -> bool) -> bool {
|
||||
self.has_focus
|
||||
&& self.is_area_focused()
|
||||
&& self.ctx.inputs.iter().any(|event| {
|
||||
if let Event::Key(key) = event {
|
||||
return f(*key);
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_global_input(&mut self, f: impl Fn(Key) -> bool) -> bool {
|
||||
self.has_focus
|
||||
&& self.ctx.inputs.iter().any(|event| {
|
||||
if let Event::Key(key) = event {
|
||||
return f(*key);
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_input(&mut self, f: impl Fn(Key) -> bool) -> Option<Key> {
|
||||
if self.has_focus && self.is_area_focused() {
|
||||
let matches = |&event| {
|
||||
if let Event::Key(key) = event {
|
||||
return f(key);
|
||||
}
|
||||
false
|
||||
};
|
||||
|
||||
if let Some(Event::Key(key)) =
|
||||
self.ctx.inputs.iter().find(|event| matches(event)).copied()
|
||||
{
|
||||
return Some(key);
|
||||
}
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Default for Ui<M> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: Theme::default(),
|
||||
area: Rect::default(),
|
||||
layout: Layout::default(),
|
||||
focus_area: None,
|
||||
has_focus: true,
|
||||
count: 0,
|
||||
ctx: Context::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Ui<M> {
|
||||
pub fn new(area: Rect) -> Self {
|
||||
Self {
|
||||
area,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_area(mut self, area: Rect) -> Self {
|
||||
self.area = area;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_layout(mut self, layout: Layout) -> Self {
|
||||
self.layout = layout;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_area_focus(mut self, focus: Option<usize>) -> Self {
|
||||
self.focus_area = focus;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_ctx(mut self, ctx: Context<M>) -> Self {
|
||||
self.ctx = ctx;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_focus(mut self) -> Self {
|
||||
self.has_focus = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn without_focus(mut self) -> Self {
|
||||
self.has_focus = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn theme(&self) -> &Theme {
|
||||
&self.theme
|
||||
}
|
||||
|
||||
pub fn area(&self) -> Rect {
|
||||
self.area
|
||||
}
|
||||
|
||||
pub fn next_area(&mut self) -> Option<(Rect, bool)> {
|
||||
let area_focus = self
|
||||
.focus_area
|
||||
.map(|focus| self.count == focus)
|
||||
.unwrap_or(false);
|
||||
let rect = self.layout.split(self.area).get(self.count).cloned();
|
||||
|
||||
self.count += 1;
|
||||
|
||||
rect.map(|rect| (rect, area_focus))
|
||||
}
|
||||
|
||||
pub fn current_area(&mut self) -> Option<(Rect, bool)> {
|
||||
let count = self.count.saturating_sub(1);
|
||||
|
||||
let area_focus = self.focus_area.map(|focus| count == focus).unwrap_or(false);
|
||||
let rect = self.layout.split(self.area).get(self.count).cloned();
|
||||
|
||||
rect.map(|rect| (rect, area_focus))
|
||||
}
|
||||
|
||||
pub fn is_area_focused(&self) -> bool {
|
||||
let count = self.count.saturating_sub(1);
|
||||
self.focus_area.map(|focus| count == focus).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
|
||||
pub fn focus_next(&mut self) {
|
||||
if self.focus_area.is_none() {
|
||||
self.focus_area = Some(0);
|
||||
} else {
|
||||
self.focus_area = Some(self.focus_area.unwrap().saturating_add(1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_message(&self, message: M) {
|
||||
if let Some(sender) = &self.ctx.sender {
|
||||
let _ = sender.send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Ui<M>
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
pub fn add(&mut self, frame: &mut Frame, widget: impl Widget) -> Response {
|
||||
widget.ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn child_ui(&mut self, area: Rect, layout: impl Into<Layout>) -> Self {
|
||||
Ui::default()
|
||||
.with_area(area)
|
||||
.with_layout(layout.into())
|
||||
.with_ctx(self.ctx.clone())
|
||||
}
|
||||
|
||||
pub fn layout<R>(
|
||||
&mut self,
|
||||
layout: impl Into<Layout>,
|
||||
focus: Option<usize>,
|
||||
add_contents: impl FnOnce(&mut Self) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.layout_dyn(layout, focus, Box::new(add_contents))
|
||||
}
|
||||
|
||||
pub fn layout_dyn<R>(
|
||||
&mut self,
|
||||
layout: impl Into<Layout>,
|
||||
focus: Option<usize>,
|
||||
add_contents: Box<AddContentFn<M, R>>,
|
||||
) -> InnerResponse<R> {
|
||||
let (area, area_focus) = self.next_area().unwrap_or_default();
|
||||
|
||||
let mut child_ui = Ui {
|
||||
has_focus: area_focus,
|
||||
focus_area: focus,
|
||||
..self.child_ui(area, layout)
|
||||
};
|
||||
|
||||
InnerResponse::new(add_contents(&mut child_ui), Response::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Ui<M>
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
pub fn container<R>(
|
||||
&mut self,
|
||||
layout: impl Into<Layout>,
|
||||
focus: &mut Option<usize>,
|
||||
add_contents: impl FnOnce(&mut Ui<M>) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
let (area, area_focus) = self.next_area().unwrap_or_default();
|
||||
|
||||
let layout: Layout = layout.into();
|
||||
let len = layout.len();
|
||||
|
||||
// TODO(erikli): Check if setting the focus area is needed at all.
|
||||
let mut child_ui = Ui {
|
||||
has_focus: area_focus,
|
||||
focus_area: *focus,
|
||||
..self.child_ui(area, layout)
|
||||
};
|
||||
|
||||
widget::Container::new(len, focus).show(&mut child_ui, add_contents)
|
||||
}
|
||||
|
||||
pub fn popup<R>(
|
||||
&mut self,
|
||||
layout: impl Into<Layout>,
|
||||
add_contents: impl FnOnce(&mut Ui<M>) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
let layout: Layout = layout.into();
|
||||
let areas = layout.split(self.area());
|
||||
let area = areas.first().cloned().unwrap_or(self.area());
|
||||
|
||||
let mut child_ui = self.child_ui(area, layout::fill());
|
||||
child_ui.has_focus = true;
|
||||
|
||||
widget::Popup::default().show(&mut child_ui, add_contents)
|
||||
}
|
||||
|
||||
pub fn label<'a>(&mut self, frame: &mut Frame, content: impl Into<Text<'a>>) -> Response {
|
||||
widget::Label::new(content).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn overline(&mut self, frame: &mut Frame) -> Response {
|
||||
let overline = String::from("▔").repeat(256);
|
||||
self.label(frame, Span::raw(overline).cyan())
|
||||
}
|
||||
|
||||
pub fn separator(&mut self, frame: &mut Frame) -> Response {
|
||||
let overline = String::from("─").repeat(256);
|
||||
self.label(
|
||||
frame,
|
||||
Span::raw(overline).fg(self.theme.border_style.fg.unwrap_or_default()),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn table<'a, R, const W: usize>(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
selected: &mut Option<usize>,
|
||||
items: &'a Vec<R>,
|
||||
columns: Vec<Column<'a>>,
|
||||
empty_message: Option<String>,
|
||||
spacing: Spacing,
|
||||
borders: Option<Borders>,
|
||||
) -> Response
|
||||
where
|
||||
R: ToRow<W> + Clone,
|
||||
{
|
||||
widget::Table::new(selected, items, columns, empty_message, borders)
|
||||
.spacing(spacing)
|
||||
.ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn tree<R, Id>(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
items: &'_ Vec<R>,
|
||||
opened: &mut Option<HashSet<Vec<Id>>>,
|
||||
selected: &mut Option<Vec<Id>>,
|
||||
borders: Option<Borders>,
|
||||
) -> Response
|
||||
where
|
||||
R: ToTree<Id> + Clone,
|
||||
Id: ToString + Clone + Eq + Hash,
|
||||
{
|
||||
widget::Tree::new(items, opened, selected, borders, false).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn shortcuts(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
shortcuts: &[(&str, &str)],
|
||||
divider: char,
|
||||
alignment: Alignment,
|
||||
) -> Response {
|
||||
widget::Shortcuts::new(shortcuts, divider, alignment).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn column_bar(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
columns: Vec<Column<'_>>,
|
||||
spacing: Spacing,
|
||||
borders: Option<Borders>,
|
||||
) -> Response {
|
||||
widget::ColumnBar::new(columns, spacing, borders).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn text_view<'a>(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
text: impl Into<Text<'a>>,
|
||||
scroll: &'a mut Position,
|
||||
borders: Option<Borders>,
|
||||
) -> Response {
|
||||
widget::TextView::new(text, None::<String>, scroll, borders).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn text_view_with_footer<'a>(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
text: impl Into<Text<'a>>,
|
||||
footer: impl Into<Text<'a>>,
|
||||
scroll: &'a mut Position,
|
||||
borders: Option<Borders>,
|
||||
) -> Response {
|
||||
widget::TextView::new(text, Some(footer), scroll, borders).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn centered_text_view<'a>(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
text: impl Into<Text<'a>>,
|
||||
borders: Option<Borders>,
|
||||
) -> Response {
|
||||
widget::CenteredTextView::new(text, borders).ui(self, frame)
|
||||
}
|
||||
|
||||
pub fn text_edit_singleline(
|
||||
&mut self,
|
||||
frame: &mut Frame,
|
||||
text: &mut String,
|
||||
cursor: &mut usize,
|
||||
label: Option<impl ToString>,
|
||||
borders: Option<Borders>,
|
||||
) -> Response {
|
||||
match label {
|
||||
Some(label) => widget::TextEdit::new(text, cursor, borders)
|
||||
.with_label(label)
|
||||
.ui(self, frame),
|
||||
_ => widget::TextEdit::new(text, cursor, borders).ui(self, frame),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user