mirror of
https://seed.radicle.dev/rad:z39mP9rQAaGmERfUMPULfPUi473tY.git
synced 2026-11-15 16:00:38 +08:00
lib: Introduce and use base view
This commit is contained in:
+47
-37
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user