diff --git a/src/ui/widget.rs b/src/ui/widget.rs index 54db68e7..1de4b2cd 100644 --- a/src/ui/widget.rs +++ b/src/ui/widget.rs @@ -23,6 +23,20 @@ pub type BoxedWidget = Box>; pub type UpdateCallback = fn(&S) -> Box; pub type EventCallback = fn(&dyn Any, UnboundedSender); +pub struct BaseView +where + P: Properties, +{ + /// Internal properties + pub props: P, + /// Message sender + pub action_tx: UnboundedSender, + /// Custom update handler + pub on_update: Option>, + /// Additional custom event handler + pub on_event: Option>, +} + /// 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 where B: Backend, { - /// Internal properties - props: WindowProps, - /// Message sender - _action_tx: UnboundedSender, - /// Custom update handler - on_update: Option>, - /// Additional custom event handler - on_event: Option>, + /// Internal base + base: BaseView>, /// All pages known pages: HashMap>, } @@ -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) -> Self { - self.on_update = Some(callback); + self.base.on_update = Some(callback); self } fn on_event(mut self, callback: EventCallback) -> 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>) { 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 { - /// Internal properties - props: ShortcutsProps, - /// Message sender - _action_tx: UnboundedSender, - /// Custom update handler - on_update: Option>, - /// Additional custom event handler - on_event: Option>, + /// Internal base + base: BaseView, } impl Shortcuts { 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 Shortcuts { impl View for Shortcuts { fn new(_state: &S, action_tx: UnboundedSender) -> 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) -> Self { - self.on_event = Some(callback); + self.base.on_event = Some(callback); self } fn on_update(mut self, callback: UpdateCallback) -> 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![]; diff --git a/src/ui/widget/container.rs b/src/ui/widget/container.rs index 3ac638cb..ce6012c4 100644 --- a/src/ui/widget/container.rs +++ b/src/ui/widget/container.rs @@ -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, - /// Custom update handler - on_update: Option>, - /// Additional custom event handler - on_event: Option>, +pub struct Header<'a: 'static, S, A> { + /// Internal base + base: BaseView>, } impl<'a, S, A> Header<'a, S, A> { pub fn columns(mut self, columns: Vec>) -> 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 for Header<'a, S, A> { fn new(_state: &S, action_tx: UnboundedSender) -> 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) -> Self { - self.on_update = Some(callback); + self.base.on_update = Some(callback); self } fn on_event(mut self, callback: EventCallback) -> 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>) { let props = props .and_then(HeaderProps::from_boxed_any) - .unwrap_or(self.props.clone()); + .unwrap_or(self.base.props.clone()); let widths: Vec = props .columns diff --git a/src/ui/widget/input.rs b/src/ui/widget/input.rs index 2541096c..ed210386 100644 --- a/src/ui/widget/input.rs +++ b/src/ui/widget/input.rs @@ -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 { - /// Internal props - props: TextFieldProps, - /// Message sender - action_tx: UnboundedSender, - /// Custom update handler - on_update: Option>, - /// Additional custom event handler - on_event: Option>, + /// Internal base + base: BaseView, /// Internal state state: TextFieldState, } @@ -133,10 +127,12 @@ impl TextField { impl View for TextField { fn new(_state: &S, action_tx: UnboundedSender) -> 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 View for TextField { } fn on_update(mut self, callback: UpdateCallback) -> Self { - self.on_update = Some(callback); + self.base.on_update = Some(callback); self } fn on_event(mut self, callback: EventCallback) -> 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::() { - 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 View for TextField { _ => {} } - 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>) { 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); diff --git a/src/ui/widget/text.rs b/src/ui/widget/text.rs index a9f1743e..8b93973f 100644 --- a/src/ui/widget/text.rs +++ b/src/ui/widget/text.rs @@ -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, - /// Custom update handler - on_update: Option>, - /// Additional custom event handler - on_event: Option>, +pub struct Paragraph<'a: 'static, S, A> { + /// Internal base + base: BaseView>, /// 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 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 for Paragraph<'a, S, A> { } fn on_event(mut self, callback: EventCallback) -> Self { - self.on_event = Some(callback); + self.base.on_event = Some(callback); self } fn on_update(mut self, callback: UpdateCallback) -> 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 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>) { 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)