Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
lib: Widgets manage view props
Erik Kundt committed 1 year ago
commit bf602d0baa9bae65efed43f695c22297c792296f
parent d3583f4c5dddb302223462d6592bd7ab635c825d
7 files changed +209 -264
modified src/ui.rs
@@ -90,7 +90,7 @@ impl Frontend {
                    break Ok(interrupted);
                }
            }
-
            terminal.draw(|frame| root.render(frame, RenderProps::from(frame.size())))?;
+
            terminal.draw(|frame| root.render(RenderProps::from(frame.size()), frame))?;
        };

        terminal::restore(&mut terminal)?;
modified src/ui/widget.rs
@@ -26,18 +26,19 @@ pub struct ViewProps {
}

impl ViewProps {
-
    pub fn new(inner: &'static dyn Any) -> Self {
-
        Self {
-
            inner: Box::new(inner),
-
        }
-
    }
-

    pub fn inner<T>(self) -> Option<T>
    where
-
        T: Clone + 'static,
+
        T: Default + Clone + 'static,
    {
        self.inner.downcast::<T>().ok().map(|inner| *inner)
    }
+

+
    pub fn inner_ref<T>(&self) -> Option<&T>
+
    where
+
        T: Default + Clone + 'static,
+
    {
+
        self.inner.downcast_ref::<T>()
+
    }
}

impl From<Box<dyn Any>> for ViewProps {
@@ -46,6 +47,14 @@ impl From<Box<dyn Any>> for ViewProps {
    }
}

+
impl From<&'static dyn Any> for ViewProps {
+
    fn from(inner: &'static dyn Any) -> Self {
+
        Self {
+
            inner: Box::new(inner),
+
        }
+
    }
+
}
+

/// A `ViewState` is the representation of a `View`s internal state. e.g. current
/// table selection or contents of a text field.
pub enum ViewState {
@@ -112,19 +121,21 @@ pub trait View {
    type State;
    type Message;

+
    /// Should return the internal state.
+
    fn view_state(&self) -> Option<ViewState> {
+
        None
+
    }
+

    /// Should handle key events and call `handle_event` on all children.
-
    fn handle_event(&mut self, key: Key) -> Option<Self::Message>;
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, _key: Key) -> Option<Self::Message> {
+
        None
+
    }

    /// Should update the internal props of this and all children.
-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>);
+
    fn update(&mut self, _props: Option<&ViewProps>, _state: &Self::State) {}

    /// Should render the view using the given `RenderProps`.
-
    fn render(&self, frame: &mut Frame, props: RenderProps);
-

-
    /// Should return the internal state.
-
    fn view_state(&self) -> Option<ViewState> {
-
        None
-
    }
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame);
}

/// A `View` needs to wrapped into a `Widget` before being able to use with the
@@ -132,6 +143,7 @@ pub trait View {
/// care of calling them before / after calling into the `View`.
pub struct Widget<S, M> {
    view: BoxedView<S, M>,
+
    props: Option<ViewProps>,
    sender: UnboundedSender<M>,
    on_update: Option<UpdateCallback<S>>,
    on_event: Option<EventCallback<M>>,
@@ -145,6 +157,7 @@ impl<S: 'static, M: 'static> Widget<S, M> {
    {
        Self {
            view: Box::new(view),
+
            props: None,
            sender: sender.clone(),
            on_update: None,
            on_event: None,
@@ -154,7 +167,7 @@ impl<S: 'static, M: 'static> Widget<S, M> {
    /// Calls `handle_event` on the wrapped view as well as the `on_event` callback.
    /// Sends any message returned by either the view or the callback.
    pub fn handle_event(&mut self, key: Key) {
-
        if let Some(message) = self.view.handle_event(key) {
+
        if let Some(message) = self.view.handle_event(self.props.as_ref(), key) {
            let _ = self.sender.send(message);
        }

@@ -174,13 +187,13 @@ impl<S: 'static, M: 'static> Widget<S, M> {
    /// props directly via their state converters, whereas library widgets can just fallback
    /// to their current props.
    pub fn update(&mut self, state: &S) {
-
        let props = self.on_update.map(|on_update| (on_update)(state));
-
        self.view.update(state, props);
+
        self.props = self.on_update.map(|on_update| (on_update)(state));
+
        self.view.update(self.props.as_ref(), state);
    }

    /// Renders the wrapped view.
-
    pub fn render(&self, frame: &mut Frame, props: RenderProps) {
-
        self.view.render(frame, props);
+
    pub fn render(&self, render: RenderProps, frame: &mut Frame) {
+
        self.view.render(self.props.as_ref(), render, frame);
    }

    /// Sets the optional custom event handler.
modified src/ui/widget/container.rs
@@ -63,52 +63,30 @@ impl<'a> Default for HeaderProps<'a> {
    }
}

-
pub struct Header<'a: 'static, S, M> {
-
    /// Internal props
-
    props: HeaderProps<'a>,
+
pub struct Header<S, M> {
    /// Phantom
    phantom: PhantomData<(S, M)>,
}

-
impl<'a, S, M> Default for Header<'a, S, M> {
+
impl<S, M> Default for Header<S, M> {
    fn default() -> Self {
        Self {
-
            props: HeaderProps::default(),
            phantom: PhantomData,
        }
    }
}

-
impl<'a, S, A> Header<'a, S, A> {
-
    pub fn columns(mut self, columns: Vec<Column<'a>>) -> Self {
-
        self.props.columns = columns;
-
        self
-
    }
-

-
    pub fn cutoff(mut self, cutoff: usize, cutoff_after: usize) -> Self {
-
        self.props.cutoff = cutoff;
-
        self.props.cutoff_after = cutoff_after;
-
        self
-
    }
-
}
-

-
impl<'a: 'static, S, M> View for Header<'a, S, M> {
+
impl<'a: 'static, S, M> View for Header<S, M> {
    type Message = M;
    type State = S;

-
    fn handle_event(&mut self, _key: Key) -> Option<Self::Message> {
-
        None
-
    }
-

-
    fn update(&mut self, _state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<HeaderProps>()) {
-
            self.props = props;
-
        }
-
    }
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = HeaderProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<HeaderProps>())
+
            .unwrap_or(&default);

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        let widths: Vec<Constraint> = self
-
            .props
+
        let widths: Vec<Constraint> = props
            .columns
            .iter()
            .filter_map(|column| {
@@ -119,8 +97,7 @@ impl<'a: 'static, S, M> View for Header<'a, S, M> {
                }
            })
            .collect();
-
        let cells = self
-
            .props
+
        let cells = props
            .columns
            .iter()
            .filter_map(|column| {
@@ -132,11 +109,8 @@ impl<'a: 'static, S, M> View for Header<'a, S, M> {
            })
            .collect::<Vec<_>>();

-
        let widths = if props.area.width < self.props.cutoff as u16 {
-
            widths
-
                .iter()
-
                .take(self.props.cutoff_after)
-
                .collect::<Vec<_>>()
+
        let widths = if render.area.width < props.cutoff as u16 {
+
            widths.iter().take(props.cutoff_after).collect::<Vec<_>>()
        } else {
            widths.iter().collect::<Vec<_>>()
        };
@@ -144,7 +118,7 @@ impl<'a: 'static, S, M> View for Header<'a, S, M> {
        // Render header
        let block = HeaderBlock::default()
            .borders(Borders::ALL)
-
            .border_style(style::border(props.focus))
+
            .border_style(style::border(render.focus))
            .border_type(BorderType::Rounded);

        let header_layout = Layout::default()
@@ -152,7 +126,7 @@ impl<'a: 'static, S, M> View for Header<'a, S, M> {
            .constraints(vec![Constraint::Min(1)])
            .vertical_margin(1)
            .horizontal_margin(1)
-
            .split(props.area);
+
            .split(render.area);

        let header = Row::new(cells).style(style::reset().bold());
        let header = ratatui::widgets::Table::default()
@@ -160,7 +134,7 @@ impl<'a: 'static, S, M> View for Header<'a, S, M> {
            .header(header)
            .widths(widths.clone());

-
        frame.render_widget(block, props.area);
+
        frame.render_widget(block, render.area);
        frame.render_widget(header, header_layout[0]);
    }
}
@@ -195,34 +169,20 @@ impl<'a> Default for FooterProps<'a> {
    }
}

-
pub struct Footer<'a, S, M> {
-
    /// Internal props
-
    props: FooterProps<'a>,
+
pub struct Footer<S, M> {
    /// Phantom
    phantom: PhantomData<(S, M)>,
}

-
impl<'a, S, M> Default for Footer<'a, S, M> {
+
impl<S, M> Default for Footer<S, M> {
    fn default() -> Self {
        Self {
-
            props: FooterProps::default(),
            phantom: PhantomData,
        }
    }
}

-
impl<'a, S, M> Footer<'a, S, M> {
-
    pub fn columns(mut self, columns: Vec<Column<'a>>) -> Self {
-
        self.props.columns = columns;
-
        self
-
    }
-

-
    pub fn cutoff(mut self, cutoff: usize, cutoff_after: usize) -> Self {
-
        self.props.cutoff = cutoff;
-
        self.props.cutoff_after = cutoff_after;
-
        self
-
    }
-

+
impl<'a, S, M> Footer<S, M> {
    fn render_cell(
        &self,
        frame: &mut ratatui::Frame,
@@ -246,23 +206,17 @@ impl<'a, S, M> Footer<'a, S, M> {
    }
}

-
impl<'a: 'static, S, M> View for Footer<'a, S, M> {
+
impl<'a: 'static, S, M> View for Footer<S, M> {
    type Message = M;
    type State = S;

-
    fn handle_event(&mut self, _key: Key) -> Option<Self::Message> {
-
        None
-
    }
-

-
    fn update(&mut self, _state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<FooterProps>()) {
-
            self.props = props;
-
        }
-
    }
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = FooterProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<FooterProps>())
+
            .unwrap_or(&default);

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        let widths = self
-
            .props
+
        let widths = props
            .columns
            .iter()
            .map(|c| match c.width {
@@ -271,9 +225,8 @@ impl<'a: 'static, S, M> View for Footer<'a, S, M> {
            })
            .collect::<Vec<_>>();

-
        let layout = Layout::horizontal(widths).split(props.area);
-
        let cells = self
-
            .props
+
        let layout = Layout::horizontal(widths).split(render.area);
+
        let cells = props
            .columns
            .iter()
            .map(|c| c.text.clone())
@@ -290,7 +243,7 @@ impl<'a: 'static, S, M> View for Footer<'a, S, M> {
                _ if i == last => FooterBlockType::End,
                _ => FooterBlockType::Repeat,
            };
-
            self.render_cell(frame, *area, block_type, cell.clone(), props.focus);
+
            self.render_cell(frame, *area, block_type, cell.clone(), render.focus);
        }
    }
}
@@ -308,8 +261,6 @@ impl ContainerProps {
}

pub struct Container<S, M> {
-
    /// Internal props
-
    props: ContainerProps,
    /// Container header
    header: Option<Widget<S, M>>,
    /// Content widget
@@ -321,7 +272,6 @@ pub struct Container<S, M> {
impl<S, M> Default for Container<S, M> {
    fn default() -> Self {
        Self {
-
            props: ContainerProps::default(),
            header: None,
            content: None,
            footer: None,
@@ -354,7 +304,7 @@ where
    type Message = M;
    type State = S;

-
    fn handle_event(&mut self, key: termion::event::Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
        if let Some(content) = &mut self.content {
            content.handle_event(key);
        }
@@ -362,11 +312,7 @@ where
        None
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<ContainerProps>()) {
-
            self.props = props;
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        if let Some(header) = &mut self.header {
            header.update(state);
        }
@@ -380,9 +326,14 @@ where
        }
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = ContainerProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<ContainerProps>())
+
            .unwrap_or(&default);
+

        let header_h = if self.header.is_some() { 3 } else { 0 };
-
        let footer_h = if self.footer.is_some() && !self.props.hide_footer {
+
        let footer_h = if self.footer.is_some() && !props.hide_footer {
            3
        } else {
            0
@@ -393,11 +344,11 @@ where
            Constraint::Min(1),
            Constraint::Length(footer_h),
        ])
-
        .areas(props.area);
+
        .areas(render.area);

        let borders = match (
            self.header.is_some(),
-
            (self.footer.is_some() && !self.props.hide_footer),
+
            (self.footer.is_some() && !props.hide_footer),
        ) {
            (false, false) => Borders::ALL,
            (true, false) => Borders::BOTTOM | Borders::LEFT | Borders::RIGHT,
@@ -406,24 +357,24 @@ where
        };

        let block = Block::default()
-
            .border_style(style::border(props.focus))
+
            .border_style(style::border(render.focus))
            .border_type(BorderType::Rounded)
            .borders(borders);
        frame.render_widget(block.clone(), content_area);

        if let Some(header) = &self.header {
-
            header.render(frame, RenderProps::from(header_area).focus(props.focus));
+
            header.render(RenderProps::from(header_area).focus(render.focus), frame);
        }

        if let Some(content) = &self.content {
            content.render(
+
                RenderProps::from(block.inner(content_area)).focus(render.focus),
                frame,
-
                RenderProps::from(block.inner(content_area)).focus(props.focus),
            );
        }

        if let Some(footer) = &self.footer {
-
            footer.render(frame, RenderProps::from(footer_area).focus(props.focus));
+
            footer.render(RenderProps::from(footer_area).focus(render.focus), frame);
        }
    }
}
@@ -448,8 +399,6 @@ impl SectionGroupProps {
}

pub struct SectionGroup<S, M> {
-
    /// Internal table properties
-
    props: SectionGroupProps,
    /// All sections
    sections: Vec<Widget<S, M>>,
    /// Internal selection and offset state
@@ -459,7 +408,6 @@ pub struct SectionGroup<S, M> {
impl<S, M> Default for SectionGroup<S, M> {
    fn default() -> Self {
        Self {
-
            props: SectionGroupProps::default(),
            sections: vec![],
            state: SectionGroupState { focus: Some(0) },
        }
@@ -499,7 +447,12 @@ where
    type State = S;
    type Message = M;

-
    fn handle_event(&mut self, key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
        let default = SectionGroupProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<SectionGroupProps>())
+
            .unwrap_or(&default);
+

        if let Some(section) = self
            .state
            .focus
@@ -508,7 +461,7 @@ where
            section.handle_event(key);
        }

-
        if self.props.handle_keys {
+
        if props.handle_keys {
            match key {
                Key::Left => {
                    self.prev();
@@ -523,18 +476,14 @@ where
        None
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<SectionGroupProps>()) {
-
            self.props = props;
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        for section in &mut self.sections {
            section.update(state);
        }
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        let areas = props.layout.split(props.area);
+
    fn render(&self, _props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let areas = render.layout.split(render.area);

        for (index, area) in areas.iter().enumerate() {
            if let Some(section) = self.sections.get(index) {
@@ -544,7 +493,7 @@ where
                    .map(|focus_index| index == focus_index)
                    .unwrap_or_default();

-
                section.render(frame, RenderProps::from(*area).focus(focus));
+
                section.render(RenderProps::from(*area).focus(focus), frame);
            }
        }
    }
modified src/ui/widget/input.rs
@@ -1,5 +1,6 @@
use std::marker::PhantomData;

+
use ratatui::Frame;
use termion::event::Key;

use ratatui::layout::{Constraint, Layout};
@@ -53,8 +54,6 @@ struct TextFieldState {
}

pub struct TextField<S, M> {
-
    /// Internal props
-
    props: TextFieldProps,
    /// Internal state
    state: TextFieldState,
    /// Phantom
@@ -64,7 +63,6 @@ pub struct TextField<S, M> {
impl<S, M> Default for TextField<S, M> {
    fn default() -> Self {
        Self {
-
            props: TextFieldProps::default(),
            state: TextFieldState {
                text: None,
                cursor_position: 0,
@@ -144,7 +142,14 @@ where
    type Message = M;
    type State = S;

-
    fn handle_event(&mut self, key: Key) -> Option<Self::Message> {
+
    fn view_state(&self) -> Option<ViewState> {
+
        self.state
+
            .text
+
            .as_ref()
+
            .map(|text| ViewState::String(text.to_string()))
+
    }
+

+
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
        match key {
            Key::Char(to_insert)
                if (key != Key::Alt('\n'))
@@ -168,28 +173,34 @@ where
        None
    }

-
    fn update(&mut self, _state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<TextFieldProps>()) {
-
            self.props = props;
+
    fn update(&mut self, props: Option<&ViewProps>, _state: &Self::State) {
+
        let default = TextFieldProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<TextFieldProps>())
+
            .unwrap_or(&default);

-
            if self.state.text.is_none() {
-
                self.state.cursor_position = self.props.text.len().saturating_sub(1);
-
            }
-
            self.state.text = Some(self.props.text.clone());
+
        if self.state.text.is_none() {
+
            self.state.cursor_position = props.text.len().saturating_sub(1);
        }
+
        self.state.text = Some(props.text.clone());
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        let area = props.area;
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = TextFieldProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<TextFieldProps>())
+
            .unwrap_or(&default);
+

+
        let area = render.area;
        let layout = Layout::vertical(Constraint::from_lengths([1, 1])).split(area);

        let text = self.state.text.clone().unwrap_or_default();
        let input = text.as_str();
-
        let label = format!(" {} ", self.props.title);
+
        let label = format!(" {} ", props.title);
        let overline = String::from("▔").repeat(area.width as usize);
        let cursor_pos = self.state.cursor_position as u16;

-
        if self.props.inline_label {
+
        if props.inline_label {
            let top_layout = Layout::horizontal([
                Constraint::Length(label.chars().count() as u16),
                Constraint::Length(1),
@@ -206,7 +217,7 @@ where
            frame.render_widget(input, top_layout[2]);
            frame.render_widget(overline, layout[1]);

-
            if self.props.show_cursor {
+
            if props.show_cursor {
                frame.set_cursor(top_layout[2].x + cursor_pos, top_layout[2].y)
            }
        } else {
@@ -222,16 +233,9 @@ where
            frame.render_widget(top, layout[0]);
            frame.render_widget(bottom, layout[1]);

-
            if self.props.show_cursor {
+
            if props.show_cursor {
                frame.set_cursor(area.x + cursor_pos, area.y)
            }
        }
    }
-

-
    fn view_state(&self) -> Option<ViewState> {
-
        self.state
-
            .text
-
            .as_ref()
-
            .map(|text| ViewState::String(text.to_string()))
-
    }
}
modified src/ui/widget/list.rs
@@ -2,6 +2,7 @@ use std::cmp;
use std::marker::PhantomData;

use ratatui::widgets::Row;
+
use ratatui::Frame;
use termion::event::Key;

use ratatui::layout::Constraint;
@@ -83,32 +84,29 @@ where
    }
}

-
pub struct Table<'a, S, M, R, const W: usize>
+
pub struct Table<S, M, R, const W: usize>
where
    R: ToRow<W>,
{
-
    /// Internal table properties
-
    props: TableProps<'a, R, W>,
    /// Internal selection and offset state
    state: TableState,
    /// Phantom
-
    phantom: PhantomData<(S, M)>,
+
    phantom: PhantomData<(S, M, R)>,
}

-
impl<'a, S, M, R, const W: usize> Default for Table<'a, S, M, R, W>
+
impl<S, M, R, const W: usize> Default for Table<S, M, R, W>
where
    R: ToRow<W>,
{
    fn default() -> Self {
        Self {
-
            props: TableProps::default(),
            state: TableState::default().with_selected(Some(0)),
            phantom: PhantomData,
        }
    }
}

-
impl<'a, S, M, R, const W: usize> Table<'a, S, M, R, W>
+
impl<S, M, R, const W: usize> Table<S, M, R, W>
where
    R: ToRow<W>,
{
@@ -163,32 +161,39 @@ where
    }
}

-
impl<'a: 'static, S: 'a, M: 'a, R, const W: usize> View for Table<'a, S, M, R, W>
+
impl<S, M, R, const W: usize> View for Table<S, M, R, W>
where
+
    S: 'static,
+
    M: 'static,
    R: ToRow<W> + Clone + 'static,
{
    type Message = M;
    type State = S;

-
    fn handle_event(&mut self, key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
        let default = TableProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<TableProps<R, W>>())
+
            .unwrap_or(&default);
+

        match key {
            Key::Up | Key::Char('k') => {
                self.prev();
            }
            Key::Down | Key::Char('j') => {
-
                self.next(self.props.items.len());
+
                self.next(props.items.len());
            }
            Key::PageUp => {
-
                self.prev_page(self.props.page_size);
+
                self.prev_page(props.page_size);
            }
            Key::PageDown => {
-
                self.next_page(self.props.items.len(), self.props.page_size);
+
                self.next_page(props.items.len(), props.page_size);
            }
            Key::Home => {
                self.begin();
            }
            Key::End => {
-
                self.end(self.props.items.len());
+
                self.end(props.items.len());
            }
            _ => {}
        }
@@ -196,41 +201,44 @@ where
        None
    }

-
    fn update(&mut self, _state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<TableProps<R, W>>()) {
-
            self.props = props;
-
        }
+
    fn update(&mut self, _props: Option<&ViewProps>, _state: &Self::State) {
+
        // TODO: Fix pre-selection
+

+
        // let default = TableProps::default();
+
        // let props = props
+
        //     .and_then(|props| props.inner_ref::<TableProps<R, W>>())
+
        //     .unwrap_or(&default);

-
        // if self.props.selected != self.state.selected() {
-
        //     self.state.select(self.props.selected);
+
        // if props.selected != self.state.selected() {
+
        //     self.state.select(props.selected);
        // }
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        let widths: Vec<Constraint> = self
-
            .props
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = TableProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<TableProps<R, W>>())
+
            .unwrap_or(&default);
+

+
        let widths: Vec<Constraint> = props
            .columns
            .iter()
            .filter_map(|c| if !c.skip { Some(c.width) } else { None })
            .collect();

-
        let widths = if props.area.width < self.props.cutoff as u16 {
-
            widths
-
                .iter()
-
                .take(self.props.cutoff_after)
-
                .collect::<Vec<_>>()
+
        let widths = if render.area.width < props.cutoff as u16 {
+
            widths.iter().take(props.cutoff_after).collect::<Vec<_>>()
        } else {
            widths.iter().collect::<Vec<_>>()
        };

-
        if !self.props.items.is_empty() {
-
            let rows = self
-
                .props
+
        if !props.items.is_empty() {
+
            let rows = props
                .items
                .iter()
                .map(|item| {
                    let mut cells = vec![];
-
                    let mut it = self.props.columns.iter();
+
                    let mut it = props.columns.iter();

                    for cell in item.to_row() {
                        if let Some(col) = it.next() {
@@ -251,9 +259,9 @@ where
                .column_spacing(1)
                .highlight_style(style::highlight());

-
            frame.render_stateful_widget(rows, props.area, &mut self.state.clone());
+
            frame.render_stateful_widget(rows, render.area, &mut self.state.clone());
        } else {
-
            let center = layout::centered_rect(props.area, 50, 10);
+
            let center = layout::centered_rect(render.area, 50, 10);
            let hint = Text::from(span::default("Nothing to show"))
                .centered()
                .light_magenta()
modified src/ui/widget/text.rs
@@ -1,5 +1,6 @@
use std::marker::PhantomData;

+
use ratatui::Frame;
use termion::event::Key;

use ratatui::layout::{Constraint, Layout};
@@ -48,19 +49,16 @@ struct ParagraphState {
    pub progress: usize,
}

-
pub struct Paragraph<'a, S, M> {
-
    /// Internal props
-
    props: ParagraphProps<'a>,
+
pub struct Paragraph<S, M> {
    /// Internal state
    state: ParagraphState,
    /// Phantom
    phantom: PhantomData<(S, M)>,
}

-
impl<'a, S, M> Default for Paragraph<'a, S, M> {
+
impl<S, M> Default for Paragraph<S, M> {
    fn default() -> Self {
        Self {
-
            props: ParagraphProps::default(),
            state: ParagraphState {
                offset: 0,
                progress: 0,
@@ -70,21 +68,11 @@ impl<'a, S, M> Default for Paragraph<'a, S, M> {
    }
}

-
impl<'a, S, M> Paragraph<'a, S, M> {
-
    pub fn scroll(&self) -> (u16, u16) {
+
impl<S, M> Paragraph<S, M> {
+
    fn scroll(&self) -> (u16, u16) {
        (self.state.offset as u16, 0)
    }

-
    pub fn page_size(mut self, page_size: usize) -> Self {
-
        self.props.page_size = page_size;
-
        self
-
    }
-

-
    pub fn text(mut self, text: &Text<'a>) -> Self {
-
        self.props.content = text.clone();
-
        self
-
    }
-

    fn prev(&mut self, len: usize, page_size: usize) -> (u16, u16) {
        self.state.offset = self.state.offset.saturating_sub(1);
        self.state.progress = Self::scroll_percent(self.state.offset, len, page_size);
@@ -140,18 +128,22 @@ impl<'a, S, M> Paragraph<'a, S, M> {
    }
}

-
impl<'a, S, M> View for Paragraph<'a, S, M>
+
impl<S, M> View for Paragraph<S, M>
where
-
    'a: 'static,
    S: 'static,
    M: 'static,
{
    type Message = M;
    type State = S;

-
    fn handle_event(&mut self, key: Key) -> Option<Self::Message> {
-
        let len = self.props.content.lines.len() + 1;
-
        let page_size = self.props.page_size;
+
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
        let default = ParagraphProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<ParagraphProps>())
+
            .unwrap_or(&default);
+

+
        let len = props.content.lines.len() + 1;
+
        let page_size = props.page_size;

        match key {
            Key::Up | Key::Char('k') => {
@@ -178,17 +170,16 @@ where
        None
    }

-
    fn update(&mut self, _state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<ParagraphProps>()) {
-
            self.props = props;
-
        }
-
    }
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = ParagraphProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<ParagraphProps>())
+
            .unwrap_or(&default);

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
        let [content_area] = Layout::horizontal([Constraint::Min(1)])
            .horizontal_margin(1)
-
            .areas(props.area);
-
        let content = ratatui::widgets::Paragraph::new(self.props.content.clone())
+
            .areas(render.area);
+
        let content = ratatui::widgets::Paragraph::new(props.content.clone())
            .scroll((self.state.offset as u16, 0));

        frame.render_widget(content, content_area);
modified src/ui/widget/window.rs
@@ -1,6 +1,7 @@
use std::hash::Hash;
use std::{collections::HashMap, marker::PhantomData};

+
use ratatui::Frame;
use termion::event::Key;

use ratatui::layout::Constraint;
@@ -31,8 +32,6 @@ impl<Id> Default for WindowProps<Id> {
}

pub struct Window<S, M, Id> {
-
    /// Internal properties
-
    props: WindowProps<Id>,
    /// All pages known
    pages: HashMap<Id, Widget<S, M>>,
}
@@ -40,7 +39,6 @@ pub struct Window<S, M, Id> {
impl<S, M, Id> Default for Window<S, M, Id> {
    fn default() -> Self {
        Self {
-
            props: WindowProps::default(),
            pages: HashMap::new(),
        }
    }
@@ -66,9 +64,13 @@ where
    type Message = M;
    type State = S;

-
    fn handle_event(&mut self, key: termion::event::Key) -> Option<Self::Message> {
-
        let page = self
-
            .props
+
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
        let default = WindowProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<WindowProps<Id>>())
+
            .unwrap_or(&default);
+

+
        let page = props
            .current_page
            .as_ref()
            .and_then(|id| self.pages.get_mut(id));
@@ -80,13 +82,13 @@ where
        None
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<WindowProps<Id>>()) {
-
            self.props = props;
-
        }
+
    fn update(&mut self, props: Option<&ViewProps>, state: &Self::State) {
+
        let default = WindowProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<WindowProps<Id>>())
+
            .unwrap_or(&default);

-
        let page = self
-
            .props
+
        let page = props
            .current_page
            .as_ref()
            .and_then(|id| self.pages.get_mut(id));
@@ -96,17 +98,21 @@ where
        }
    }

-
    fn render(&self, frame: &mut ratatui::Frame, _props: RenderProps) {
+
    fn render(&self, props: Option<&ViewProps>, _render: RenderProps, frame: &mut Frame) {
+
        let default = WindowProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<WindowProps<Id>>())
+
            .unwrap_or(&default);
+

        let area = frame.size();

-
        let page = self
-
            .props
+
        let page = props
            .current_page
            .as_ref()
            .and_then(|id| self.pages.get(id));

        if let Some(page) = page {
-
            page.render(frame, RenderProps::from(area).focus(true));
+
            page.render(RenderProps::from(area).focus(true), frame);
        }
    }
}
@@ -142,33 +148,13 @@ impl Default for ShortcutsProps {
}

pub struct Shortcuts<S, M> {
-
    /// Internal props
-
    props: ShortcutsProps,
    /// Phantom
    phantom: PhantomData<(S, M)>,
}

-
impl<S, M> Shortcuts<S, M> {
-
    pub fn divider(mut self, divider: char) -> Self {
-
        self.props.divider = divider;
-
        self
-
    }
-

-
    pub fn shortcuts(mut self, shortcuts: &[(&str, &str)]) -> Self {
-
        self.props.shortcuts.clear();
-
        for (short, long) in shortcuts {
-
            self.props
-
                .shortcuts
-
                .push((short.to_string(), long.to_string()));
-
        }
-
        self
-
    }
-
}
-

impl<S, M> Default for Shortcuts<S, M> {
    fn default() -> Self {
        Self {
-
            props: ShortcutsProps::default(),
            phantom: PhantomData,
        }
    }
@@ -178,28 +164,22 @@ impl<S, M> View for Shortcuts<S, M> {
    type Message = M;
    type State = S;

-
    fn handle_event(&mut self, _key: Key) -> Option<Self::Message> {
-
        None
-
    }
-

-
    fn update(&mut self, _state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<ShortcutsProps>()) {
-
            self.props = props;
-
        }
-
    }
-

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
        use ratatui::widgets::Table;

-
        let mut shortcuts = self.props.shortcuts.iter().peekable();
+
        let default = ShortcutsProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<ShortcutsProps>())
+
            .unwrap_or(&default);
+

+
        let mut shortcuts = props.shortcuts.iter().peekable();
        let mut row = vec![];

        while let Some(shortcut) = shortcuts.next() {
            let short = Text::from(shortcut.0.clone()).style(style::gray());
            let long = Text::from(shortcut.1.clone()).style(style::gray().dim());
            let spacer = Text::from(String::new());
-
            let divider =
-
                Text::from(format!(" {} ", self.props.divider)).style(style::gray().dim());
+
            let divider = Text::from(format!(" {} ", props.divider)).style(style::gray().dim());

            row.push((shortcut.0.chars().count(), short));
            row.push((1, spacer));
@@ -223,6 +203,6 @@ impl<S, M> View for Shortcuts<S, M> {
            .collect();

        let table = Table::new([Row::new(row)], widths).column_spacing(0);
-
        frame.render_widget(table, props.area);
+
        frame.render_widget(table, render.area);
    }
}