lib: Introduce and use base view

This commit is contained in:
Erik Kundt
2024-04-23 16:36:24 +02:00
parent 73f29c94ca
commit f7c90ac425
4 changed files with 108 additions and 107 deletions
+47 -37
View File
@@ -23,6 +23,20 @@ pub type BoxedWidget<B, S, A> = Box<dyn Widget<B, S, A>>;
pub type UpdateCallback<S> = fn(&S) -> Box<dyn Any>;
pub type EventCallback<A> = fn(&dyn Any, UnboundedSender<A>);
pub struct BaseView<S, A, P>
where
P: Properties,
{
/// Internal properties
pub props: P,
/// Message sender
pub action_tx: UnboundedSender<A>,
/// Custom update handler
pub on_update: Option<UpdateCallback<S>>,
/// Additional custom event handler
pub on_event: Option<EventCallback<A>>,
}
/// Main trait defining a `View` behaviour.
///
/// This is the first trait that you should implement to define a custom `Widget`.
@@ -138,14 +152,8 @@ pub struct Window<B, S, A, Id>
where
B: Backend,
{
/// Internal properties
props: WindowProps<Id>,
/// Message sender
_action_tx: UnboundedSender<A>,
/// Custom update handler
on_update: Option<UpdateCallback<S>>,
/// Additional custom event handler
on_event: Option<EventCallback<A>>,
/// Internal base
base: BaseView<S, A, WindowProps<Id>>,
/// All pages known
pages: HashMap<Id, BoxedWidget<B, S, A>>,
}
@@ -172,29 +180,32 @@ where
Self: Sized,
{
Self {
_action_tx: action_tx.clone(),
props: WindowProps::default(),
base: BaseView {
action_tx: action_tx.clone(),
props: WindowProps::default(),
on_update: None,
on_event: None,
},
pages: HashMap::new(),
on_update: None,
on_event: None,
}
}
fn on_update(mut self, callback: UpdateCallback<S>) -> Self {
self.on_update = Some(callback);
self.base.on_update = Some(callback);
self
}
fn on_event(mut self, callback: EventCallback<A>) -> Self {
self.on_event = Some(callback);
self.base.on_event = Some(callback);
self
}
fn update(&mut self, state: &S) {
self.props =
WindowProps::from_callback(self.on_update, state).unwrap_or(self.props.clone());
self.base.props = WindowProps::from_callback(self.base.on_update, state)
.unwrap_or(self.base.props.clone());
let page = self
.base
.props
.current_page
.as_ref()
@@ -207,6 +218,7 @@ where
fn handle_key_event(&mut self, key: termion::event::Key) {
let page = self
.base
.props
.current_page
.as_ref()
@@ -226,11 +238,12 @@ where
fn render(&self, frame: &mut ratatui::Frame, _area: Rect, props: Option<Box<dyn Any>>) {
let _props = props
.and_then(WindowProps::from_boxed_any)
.unwrap_or(self.props.clone());
.unwrap_or(self.base.props.clone());
let area = frame.size();
let page = self
.base
.props
.current_page
.as_ref()
@@ -275,26 +288,21 @@ impl Default for ShortcutsProps {
impl Properties for ShortcutsProps {}
pub struct Shortcuts<S, A> {
/// Internal properties
props: ShortcutsProps,
/// Message sender
_action_tx: UnboundedSender<A>,
/// Custom update handler
on_update: Option<UpdateCallback<S>>,
/// Additional custom event handler
on_event: Option<EventCallback<A>>,
/// Internal base
base: BaseView<S, A, ShortcutsProps>,
}
impl<S, A> Shortcuts<S, A> {
pub fn divider(mut self, divider: char) -> Self {
self.props.divider = divider;
self.base.props.divider = divider;
self
}
pub fn shortcuts(mut self, shortcuts: &[(&str, &str)]) -> Self {
self.props.shortcuts.clear();
self.base.props.shortcuts.clear();
for (short, long) in shortcuts {
self.props
self.base
.props
.shortcuts
.push((short.to_string(), long.to_string()));
}
@@ -305,28 +313,30 @@ impl<S, A> Shortcuts<S, A> {
impl<S, A> View<S, A> for Shortcuts<S, A> {
fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
Self {
_action_tx: action_tx.clone(),
props: ShortcutsProps::default(),
on_update: None,
on_event: None,
base: BaseView {
action_tx: action_tx.clone(),
props: ShortcutsProps::default(),
on_update: None,
on_event: None,
},
}
}
fn on_event(mut self, callback: EventCallback<A>) -> Self {
self.on_event = Some(callback);
self.base.on_event = Some(callback);
self
}
fn on_update(mut self, callback: UpdateCallback<S>) -> Self {
self.on_update = Some(callback);
self.base.on_update = Some(callback);
self
}
fn handle_key_event(&mut self, _key: Key) {}
fn update(&mut self, state: &S) {
self.props =
ShortcutsProps::from_callback(self.on_update, state).unwrap_or(self.props.clone());
self.base.props = ShortcutsProps::from_callback(self.base.on_update, state)
.unwrap_or(self.base.props.clone());
}
}
@@ -339,7 +349,7 @@ where
let props = props
.and_then(ShortcutsProps::from_boxed_any)
.unwrap_or(self.props.clone());
.unwrap_or(self.base.props.clone());
let mut shortcuts = props.shortcuts.iter().peekable();
let mut row = vec![];
+24 -25
View File
@@ -11,7 +11,9 @@ use ratatui::widgets::{Block, BorderType, Borders, Row};
use crate::ui::ext::{FooterBlock, FooterBlockType, HeaderBlock};
use crate::ui::theme::style;
use super::{BoxedWidget, Column, EventCallback, Properties, UpdateCallback, View, Widget};
use super::{
BaseView, BoxedWidget, Column, EventCallback, Properties, UpdateCallback, View, Widget,
};
#[derive(Clone, Debug)]
pub struct HeaderProps<'a> {
@@ -52,31 +54,25 @@ impl<'a> Default for HeaderProps<'a> {
impl<'a: 'static> Properties for HeaderProps<'a> {}
pub struct Header<'a, S, A> {
/// Internal props
props: HeaderProps<'a>,
/// Message sender
action_tx: UnboundedSender<A>,
/// Custom update handler
on_update: Option<UpdateCallback<S>>,
/// Additional custom event handler
on_event: Option<EventCallback<A>>,
pub struct Header<'a: 'static, S, A> {
/// Internal base
base: BaseView<S, A, HeaderProps<'a>>,
}
impl<'a, S, A> Header<'a, S, A> {
pub fn columns(mut self, columns: Vec<Column<'a>>) -> Self {
self.props.columns = columns;
self.base.props.columns = columns;
self
}
pub fn focus(mut self, focus: bool) -> Self {
self.props.focus = focus;
self.base.props.focus = focus;
self
}
pub fn cutoff(mut self, cutoff: usize, cutoff_after: usize) -> Self {
self.props.cutoff = cutoff;
self.props.cutoff_after = cutoff_after;
self.base.props.cutoff = cutoff;
self.base.props.cutoff_after = cutoff_after;
self
}
}
@@ -84,33 +80,36 @@ impl<'a, S, A> Header<'a, S, A> {
impl<'a: 'static, S, A> View<S, A> for Header<'a, S, A> {
fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
Self {
action_tx: action_tx.clone(),
props: HeaderProps::default(),
on_update: None,
on_event: None,
base: BaseView {
action_tx: action_tx.clone(),
props: HeaderProps::default(),
on_update: None,
on_event: None,
},
}
}
fn on_update(mut self, callback: UpdateCallback<S>) -> Self {
self.on_update = Some(callback);
self.base.on_update = Some(callback);
self
}
fn on_event(mut self, callback: EventCallback<A>) -> Self {
self.on_event = Some(callback);
self.base.on_event = Some(callback);
self
}
fn update(&mut self, state: &S) {
self.props = self
self.base.props = self
.base
.on_update
.and_then(|on_update| HeaderProps::from_boxed_any((on_update)(state)))
.unwrap_or(self.props.clone());
.unwrap_or(self.base.props.clone());
}
fn handle_key_event(&mut self, _key: Key) {
if let Some(on_event) = self.on_event {
(on_event)(&self.props, self.action_tx.clone());
if let Some(on_event) = self.base.on_event {
(on_event)(&self.base.props, self.base.action_tx.clone());
}
}
}
@@ -122,7 +121,7 @@ where
fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
let props = props
.and_then(HeaderProps::from_boxed_any)
.unwrap_or(self.props.clone());
.unwrap_or(self.base.props.clone());
let widths: Vec<Constraint> = props
.columns
+16 -20
View File
@@ -9,7 +9,7 @@ use ratatui::prelude::{Backend, Rect};
use ratatui::style::Stylize;
use ratatui::text::{Line, Span};
use super::{EventCallback, Properties, UpdateCallback, View, Widget};
use super::{BaseView, EventCallback, Properties, UpdateCallback, View, Widget};
#[derive(Clone)]
pub struct TextFieldProps {
@@ -56,14 +56,8 @@ pub struct TextFieldState {
}
pub struct TextField<S, A> {
/// Internal props
props: TextFieldProps,
/// Message sender
action_tx: UnboundedSender<A>,
/// Custom update handler
on_update: Option<UpdateCallback<S>>,
/// Additional custom event handler
on_event: Option<EventCallback<A>>,
/// Internal base
base: BaseView<S, A, TextFieldProps>,
/// Internal state
state: TextFieldState,
}
@@ -133,10 +127,12 @@ impl<S, A> TextField<S, A> {
impl<S, A> View<S, A> for TextField<S, A> {
fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
Self {
action_tx,
props: TextFieldProps::default(),
on_update: None,
on_event: None,
base: BaseView {
action_tx: action_tx.clone(),
props: TextFieldProps::default(),
on_update: None,
on_event: None,
},
state: TextFieldState {
text: None,
cursor_position: 0,
@@ -145,19 +141,19 @@ impl<S, A> View<S, A> for TextField<S, A> {
}
fn on_update(mut self, callback: UpdateCallback<S>) -> Self {
self.on_update = Some(callback);
self.base.on_update = Some(callback);
self
}
fn on_event(mut self, callback: EventCallback<A>) -> Self {
self.on_event = Some(callback);
self.base.on_event = Some(callback);
self
}
fn update(&mut self, state: &S) {
if let Some(on_update) = self.on_update {
if let Some(on_update) = self.base.on_update {
if let Some(props) = (on_update)(state).downcast_ref::<TextFieldProps>() {
self.props = props.clone();
self.base.props = props.clone();
if self.state.text.is_none() {
self.state.cursor_position = props.text.len().saturating_sub(1);
@@ -188,8 +184,8 @@ impl<S, A> View<S, A> for TextField<S, A> {
_ => {}
}
if let Some(on_event) = self.on_event {
(on_event)(&self.state, self.action_tx.clone());
if let Some(on_event) = self.base.on_event {
(on_event)(&self.state, self.base.action_tx.clone());
}
}
}
@@ -201,7 +197,7 @@ where
fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
let props = props
.and_then(TextFieldProps::from_boxed_any)
.unwrap_or(self.props.clone());
.unwrap_or(self.base.props.clone());
let layout = Layout::vertical(Constraint::from_lengths([1, 1])).split(area);
+21 -25
View File
@@ -8,7 +8,7 @@ use ratatui::backend::Backend;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::Text;
use super::{EventCallback, Properties, UpdateCallback, View, Widget};
use super::{BaseView, EventCallback, Properties, UpdateCallback, View, Widget};
#[derive(Clone)]
pub struct ParagraphProps<'a> {
@@ -59,15 +59,9 @@ pub struct ParagraphState {
pub progress: usize,
}
pub struct Paragraph<'a, S, A> {
/// Internal properties
props: ParagraphProps<'a>,
/// Message sender
action_tx: UnboundedSender<A>,
/// Custom update handler
on_update: Option<UpdateCallback<S>>,
/// Additional custom event handler
on_event: Option<EventCallback<A>>,
pub struct Paragraph<'a: 'static, S, A> {
/// Internal base
base: BaseView<S, A, ParagraphProps<'a>>,
/// Internal state
state: ParagraphState,
}
@@ -78,12 +72,12 @@ impl<'a, S, A> Paragraph<'a, S, A> {
}
pub fn page_size(mut self, page_size: usize) -> Self {
self.props.page_size = page_size;
self.base.props.page_size = page_size;
self
}
pub fn text(mut self, text: &Text<'a>) -> Self {
self.props.content = text.clone();
self.base.props.content = text.clone();
self
}
@@ -152,10 +146,12 @@ impl<'a: 'static, S, A> View<S, A> for Paragraph<'a, S, A> {
Self: Sized,
{
Self {
action_tx: action_tx.clone(),
props: ParagraphProps::default(),
on_update: None,
on_event: None,
base: BaseView {
action_tx: action_tx.clone(),
props: ParagraphProps::default(),
on_update: None,
on_event: None,
},
state: ParagraphState {
offset: 0,
progress: 0,
@@ -164,23 +160,23 @@ impl<'a: 'static, S, A> View<S, A> for Paragraph<'a, S, A> {
}
fn on_event(mut self, callback: EventCallback<A>) -> Self {
self.on_event = Some(callback);
self.base.on_event = Some(callback);
self
}
fn on_update(mut self, callback: UpdateCallback<S>) -> Self {
self.on_update = Some(callback);
self.base.on_update = Some(callback);
self
}
fn update(&mut self, state: &S) {
self.props =
ParagraphProps::from_callback(self.on_update, state).unwrap_or(self.props.clone());
self.base.props = ParagraphProps::from_callback(self.base.on_update, state)
.unwrap_or(self.base.props.clone());
}
fn handle_key_event(&mut self, key: Key) {
let len = self.props.content.lines.len() + 1;
let page_size = self.props.page_size;
let len = self.base.props.content.lines.len() + 1;
let page_size = self.base.props.page_size;
match key {
Key::Up | Key::Char('k') => {
@@ -204,8 +200,8 @@ impl<'a: 'static, S, A> View<S, A> for Paragraph<'a, S, A> {
_ => {}
}
if let Some(on_event) = self.on_event {
(on_event)(&self.state, self.action_tx.clone());
if let Some(on_event) = self.base.on_event {
(on_event)(&self.state, self.base.action_tx.clone());
}
}
}
@@ -217,7 +213,7 @@ where
fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
let props = props
.and_then(ParagraphProps::from_boxed_any)
.unwrap_or(self.props.clone());
.unwrap_or(self.base.props.clone());
let [content_area] = Layout::horizontal([Constraint::Min(1)])
.horizontal_margin(1)