Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
lib: Rework views and widgets
Erik Kundt committed 1 year ago
commit 96c8cdd675d2b2a20778681d168e7dc1e44e1485
parent 56116f388be22b298cdc62924fa1c8456d2394d4
16 files changed +434 -481
modified bin/commands/inbox/select.rs
@@ -19,7 +19,7 @@ use tui::store;
use tui::store::StateValue;
use tui::ui::items::{Filter, NotificationItem, NotificationItemFilter};
use tui::ui::widget::window::{Window, WindowProps};
-
use tui::ui::widget::{Properties, Widget};
+
use tui::ui::widget::{Properties, View};
use tui::Channel;
use tui::Exit;

modified bin/commands/inbox/select/ui.rs
@@ -22,7 +22,7 @@ use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps, TableUtils};
use tui::ui::widget::text::{Paragraph, ParagraphProps};
use tui::ui::widget::window::{Shortcuts, ShortcutsProps};
-
use tui::ui::widget::{BoxedAny, Properties, RenderProps, Widget, WidgetBase};
+
use tui::ui::widget::{BoxedAny, Properties, RenderProps, View, WidgetBase};

use tui::Selection;

@@ -122,7 +122,7 @@ pub struct Browser<'a> {
    search: BoxedWidget,
}

-
impl<'a: 'static> Widget for Browser<'a> {
+
impl<'a: 'static> View for Browser<'a> {
    type Message = Message;
    type State = State;

@@ -315,7 +315,7 @@ pub struct BrowserPage<'a> {
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static> Widget for BrowserPage<'a> {
+
impl<'a: 'static> View for BrowserPage<'a> {
    type Message = Message;
    type State = State;

@@ -410,7 +410,7 @@ pub struct Search {
    input: BoxedWidget,
}

-
impl Widget for Search {
+
impl View for Search {
    type Message = Message;
    type State = State;

@@ -513,7 +513,7 @@ pub struct HelpPage<'a> {
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static> Widget for HelpPage<'a> {
+
impl<'a: 'static> View for HelpPage<'a> {
    type Message = Message;
    type State = State;

modified bin/commands/issue/select.rs
@@ -15,7 +15,7 @@ use tui::cob::issue;
use tui::store::StateValue;
use tui::ui::items::{Filter, IssueItem, IssueItemFilter};
use tui::ui::widget::window::{Window, WindowProps};
-
use tui::ui::widget::{Properties, Widget};
+
use tui::ui::widget::{Properties, View};
use tui::Channel;

use tui::Exit;
modified bin/commands/issue/select/ui.rs
@@ -24,7 +24,7 @@ use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps, TableUtils};
use tui::ui::widget::text::{Paragraph, ParagraphProps};
use tui::ui::widget::window::{Shortcuts, ShortcutsProps};
-
use tui::ui::widget::{BoxedAny, Properties, RenderProps, Widget, WidgetBase};
+
use tui::ui::widget::{BoxedAny, Properties, RenderProps, View, WidgetBase};

use tui::Selection;

@@ -142,7 +142,7 @@ pub struct Browser<'a> {
    search: BoxedWidget,
}

-
impl<'a: 'static> Widget for Browser<'a> {
+
impl<'a: 'static> View for Browser<'a> {
    type Message = Message;
    type State = State;

@@ -275,7 +275,7 @@ impl<'a: 'static> Widget for Browser<'a> {
            self.search
                .render(frame, RenderProps::from(search_area).focus(props.focus));
        } else {
-
            self.issues.render(frame, props);
+
            self.issues.render(frame, frame, props);
        }
    }

@@ -334,7 +334,7 @@ pub struct BrowserPage<'a> {
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static> Widget for BrowserPage<'a> {
+
impl<'a: 'static> View for BrowserPage<'a> {
    type Message = Message;
    type State = State;

@@ -429,7 +429,7 @@ pub struct Search {
    input: BoxedWidget,
}

-
impl Widget for Search {
+
impl View for Search {
    type Message = Message;
    type State = State;

@@ -529,7 +529,7 @@ pub struct HelpPage<'a> {
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static> Widget for HelpPage<'a> {
+
impl<'a: 'static> View for HelpPage<'a> {
    type Message = Message;
    type State = State;

modified bin/commands/patch/select.rs
@@ -15,7 +15,7 @@ use tui::cob::patch;
use tui::store;
use tui::ui::items::{Filter, PatchItem, PatchItemFilter};
use tui::ui::widget::window::{Window, WindowProps};
-
use tui::ui::widget::{Properties, Widget};
+
use tui::ui::widget::{Properties, View};
use tui::Channel;
use tui::Exit;

modified bin/commands/patch/select/ui.rs
@@ -25,7 +25,7 @@ use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps, TableUtils};
use tui::ui::widget::text::{Paragraph, ParagraphProps};
use tui::ui::widget::window::{Shortcuts, ShortcutsProps};
-
use tui::ui::widget::{BoxedAny, Properties, RenderProps, Widget, WidgetBase};
+
use tui::ui::widget::{BoxedAny, Properties, RenderProps, View, WidgetBase};

use tui::Selection;

@@ -142,7 +142,7 @@ pub struct Browser<'a> {
    search: BoxedWidget,
}

-
impl<'a: 'static> Widget for Browser<'a> {
+
impl<'a: 'static> View for Browser<'a> {
    type Message = Message;
    type State = State;

@@ -357,7 +357,7 @@ pub struct BrowserPage<'a> {
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static> Widget for BrowserPage<'a> {
+
impl<'a: 'static> View for BrowserPage<'a> {
    type Message = Message;
    type State = State;

@@ -452,7 +452,7 @@ pub struct Search {
    input: BoxedWidget,
}

-
impl Widget for Search {
+
impl View for Search {
    type Message = Message;
    type State = State;

@@ -552,7 +552,7 @@ pub struct HelpPage<'a> {
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static> Widget for HelpPage<'a> {
+
impl<'a: 'static> View for HelpPage<'a> {
    type Message = Message;
    type State = State;

modified examples/hello.rs
@@ -5,8 +5,8 @@ use radicle_tui as tui;
use termion::event::Key;
use tui::store;
use tui::ui::widget::text::{Paragraph, ParagraphProps};
-
use tui::ui::widget::{Properties, Widget};
-
use tui::{Channel, Exit};
+
use tui::ui::widget::ToWidget;
+
use tui::{BoxedAny, Channel, Exit};

#[derive(Clone, Debug)]
struct State {
@@ -41,22 +41,19 @@ pub async fn main() -> Result<()> {
        welcome: "Hello TUI".to_string(),
    };

-
    let welcome = Paragraph::new(&state, channel.tx.clone())
-
        .on_update(|state| {
+
    let welcome = Paragraph::default()
+
        .to_widget(channel.tx.clone())
+
        .on_update(|state: &State| {
            ParagraphProps::default()
                .text(&state.welcome.clone().into())
-
                .to_boxed()
+
                .to_boxed_any()
+
                .into()
        })
-
        .on_event(|paragraph, key| {
-
            paragraph
-
                .downcast_mut::<Paragraph<'_, State, Message>>()
-
                .and_then(|paragraph| match key {
-
                    Key::Char('r') => paragraph.send(Message::ReverseWelcome).ok(),
-
                    Key::Char('q') => paragraph.send(Message::Quit).ok(),
-
                    _ => None,
-
                });
-
        })
-
        .to_boxed();
+
        .on_event(|_, key| match key {
+
            Key::Char('r') => Some(Message::ReverseWelcome),
+
            Key::Char('q') => Some(Message::Quit),
+
            _ => None,
+
        });

    tui::run(channel, state, welcome).await?;

modified src/lib.rs
@@ -8,15 +8,19 @@ pub mod task;
pub mod terminal;
pub mod ui;

+
use std::any::Any;
use std::fmt::Debug;

-
use anyhow::Result;
+
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};

use serde::ser::{Serialize, SerializeStruct, Serializer};
+

+
use anyhow::Result;
+

use store::State;
use task::Interrupted;
-
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
-
use ui::{widget::Widget, Frontend};
+
use ui::widget::Widget;
+
use ui::Frontend;

/// An optional return value.
#[derive(Clone, Debug)]
@@ -74,6 +78,36 @@ where
    }
}

+
/// Provide implementations for conversions to and from `Box<dyn Any>`.
+
pub trait BoxedAny {
+
    fn from_boxed_any(any: Box<dyn Any>) -> Option<Self>
+
    where
+
        Self: Sized + Clone + 'static;
+

+
    fn to_boxed_any(self) -> Box<dyn Any>
+
    where
+
        Self: Sized + Clone + 'static;
+
}
+

+
impl<T> BoxedAny for T
+
where
+
    T: Sized + Clone + 'static,
+
{
+
    fn from_boxed_any(any: Box<dyn Any>) -> Option<Self>
+
    where
+
        Self: Sized + Clone + 'static,
+
    {
+
        any.downcast::<Self>().ok().map(|b| *b)
+
    }
+

+
    fn to_boxed_any(self) -> Box<dyn Any>
+
    where
+
        Self: Sized + Clone + 'static,
+
    {
+
        Box::new(self)
+
    }
+
}
+

/// A 'PageStack' for applications. Page identifier can be pushed to and
/// popped from the stack.
#[derive(Clone, Default, Debug)]
@@ -120,20 +154,20 @@ impl<A> Default for Channel<A> {
/// Initialize a `Store` with the `State` given and a `Frontend` with the `Widget` given,
/// and run their main loops concurrently. Connect them to the `Channel` and also to
/// an interrupt broadcast channel also initialized in this function.
-
pub async fn run<S, M, W, P>(channel: Channel<M>, state: S, root: Box<W>) -> Result<Option<P>>
+
pub async fn run<S, M, P>(channel: Channel<M>, state: S, root: Widget<S, M>) -> Result<Option<P>>
where
    S: State<P, Message = M> + Clone + Debug + Send + Sync + 'static,
-
    W: Widget<State = S, Message = M>,
+
    M: 'static,
    P: Clone + Debug + Send + Sync + 'static,
{
    let (terminator, mut interrupt_rx) = task::create_termination();

    let (store, state_rx) = store::Store::<S, M, P>::new();
-
    let frontend = Frontend::<M>::new(channel.tx.clone());
+
    let frontend = Frontend::default();

    tokio::try_join!(
        store.main_loop(state, terminator, channel.rx, interrupt_rx.resubscribe()),
-
        frontend.main_loop(Some(*root), state_rx, interrupt_rx.resubscribe()),
+
        frontend.main_loop(root, state_rx, interrupt_rx.resubscribe()),
    )?;

    if let Ok(reason) = interrupt_rx.recv().await {
modified src/ui.rs
@@ -10,16 +10,16 @@ use std::fmt::Debug;
use std::time::Duration;

use tokio::sync::broadcast;
-
use tokio::sync::mpsc;
use tokio::sync::mpsc::UnboundedReceiver;

use crate::ui::widget::RenderProps;

+
use self::widget::Widget;
+

use super::event::Event;
use super::store::State;
use super::task::Interrupted;
use super::terminal;
-
use super::ui::widget::Widget;

const RENDERING_TICK_RATE: Duration = Duration::from_millis(250);
const INLINE_HEIGHT: usize = 20;
@@ -29,16 +29,10 @@ const INLINE_HEIGHT: usize = 20;
///
/// Once created and run with `main_loop`, the `Frontend` will wait for new messages
/// being sent on either the terminal event, the state or the interrupt message channel.
-
pub struct Frontend<M> {
-
    tx: mpsc::UnboundedSender<M>,
-
}
-

-
impl<M> Frontend<M> {
-
    /// Create a new `Frontend` storing the sending end of a message channel.
-
    pub fn new(tx: mpsc::UnboundedSender<M>) -> Self {
-
        Self { tx: tx.clone() }
-
    }
+
#[derive(Default)]
+
pub struct Frontend {}

+
impl Frontend {
    /// By calling `main_loop`, the `Frontend` will wait for new messages being sent
    /// on either the terminal event, the state or the interrupt message channel.
    /// After all, it will draw the (potentially) updated root widget.
@@ -53,15 +47,15 @@ impl<M> Frontend<M> {
    ///
    /// Interrupt messages are being sent to broadcast channel for retrieving the
    /// application kill signal.
-
    pub async fn main_loop<S, W, P>(
+
    pub async fn main_loop<S, M, P>(
        self,
-
        root: Option<W>,
+
        mut root: Widget<S, M>,
        mut state_rx: UnboundedReceiver<S>,
        mut interrupt_rx: broadcast::Receiver<Interrupted<P>>,
    ) -> anyhow::Result<Interrupted<P>>
    where
-
        S: State<P>,
-
        W: Widget<State = S, Message = M>,
+
        S: State<P> + 'static,
+
        M: 'static,
        P: Clone + Send + Sync + Debug,
    {
        let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);
@@ -72,13 +66,8 @@ impl<M> Frontend<M> {
        let mut root = {
            let state = state_rx.recv().await.unwrap();

-
            match root {
-
                Some(mut root) => {
-
                    root.update(&state);
-
                    root
-
                }
-
                None => W::new(&state, self.tx.clone()),
-
            }
+
            root.update(&state);
+
            root
        };

        let result: anyhow::Result<Interrupted<P>> = loop {
modified src/ui/items.rs
@@ -22,9 +22,13 @@ use ratatui::widgets::Cell;

use super::super::git;
use super::theme::style;
-
use super::widget::ToRow;
use super::{format, span};

+
/// Needs to be implemented for items that are supposed to be rendered in tables.
+
pub trait ToRow<const W: usize> {
+
    fn to_row(&self) -> [Cell; W];
+
}
+

pub trait Filter<T> {
    fn matches(&self, item: &T) -> bool;
}
modified src/ui/widget.rs
@@ -6,48 +6,70 @@ pub mod window;

use std::any::Any;

-
use tokio::sync::mpsc::error::SendError;
use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;

use ratatui::prelude::*;
-
use ratatui::widgets::Cell;
-

-
pub type BoxedWidget<S, M> = Box<dyn Widget<State = S, Message = M>>;
-

-
pub type UpdateCallback<S> = fn(&S) -> Box<dyn Any>;
-
pub type EventCallback = fn(&mut dyn Any, Key);
-

-
/// A `WidgetBase` provides common functionality to a `Widget`. It's used to store
-
/// event and update callbacks as well sending messages to the UI's message channel.
-
pub struct WidgetBase<S, M> {
-
    /// Message sender
-
    pub tx: UnboundedSender<M>,
-
    /// Custom update handler
-
    pub on_update: Option<UpdateCallback<S>>,
-
    /// Additional custom event handler
-
    pub on_event: Option<EventCallback>,
+

+
pub type BoxedView<S, M> = Box<dyn View<State = S, Message = M>>;
+
pub type UpdateCallback<S> = fn(&S) -> ViewProps;
+
pub type EventCallback<M> = fn(Option<&ViewState>, Key) -> Option<M>;
+

+
/// `ViewProps` are properties of a `View`. They define a `View`s data, configuration etc.
+
/// Since the framework itself does not know the concrete type of `View`, it also does not
+
/// know the concrete type of a `View`s properties.
+
/// Hence, view properties are stored inside a `Box<dyn Any>` and downcasted to the concrete
+
/// type when needed.
+
pub struct ViewProps {
+
    inner: Box<dyn Any>,
}

-
impl<S, M> WidgetBase<S, M> {
-
    /// Create a new `WidgetBase` with no callbacks set.
-
    pub fn new(tx: UnboundedSender<M>) -> Self {
+
impl ViewProps {
+
    pub fn new(inner: &'static dyn Any) -> Self {
        Self {
-
            tx: tx.clone(),
-
            on_update: None,
-
            on_event: None,
+
            inner: Box::new(inner),
        }
    }

-
    /// Send a message to the internal channel.
-
    pub fn send(&self, message: M) -> Result<(), SendError<M>> {
-
        self.tx.send(message)
+
    pub fn inner<T>(self) -> Option<T>
+
    where
+
        T: Clone + 'static,
+
    {
+
        self.inner.downcast::<T>().ok().map(|inner| *inner)
+
    }
+
}
+

+
impl From<Box<dyn Any>> for ViewProps {
+
    fn from(props: Box<dyn Any>) -> Self {
+
        ViewProps { inner: props }
+
    }
+
}
+

+
/// 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 {
+
    USize(usize),
+
    String(String),
+
}
+

+
impl ViewState {
+
    pub fn unwrap_usize(&self) -> Option<usize> {
+
        match self {
+
            ViewState::USize(value) => Some(*value),
+
            _ => None,
+
        }
+
    }
+

+
    pub fn unwrap_string(&self) -> Option<String> {
+
        match self {
+
            ViewState::String(value) => Some(value.clone()),
+
            _ => None,
+
        }
    }
}

-
/// General properties that specify how a `Widget` is rendered.
-
/// They can be passed to a widgets' `render` function.
+
/// General properties that specify how a `View` is rendered.
#[derive(Clone, Default)]
pub struct RenderProps {
    /// Area of the render props.
@@ -82,27 +104,67 @@ impl From<Rect> for RenderProps {
    }
}

-
/// Main trait defining a `Widget` behaviour.
-
///
-
/// This is the trait that you should implement to define a custom `Widget`.
-
pub trait Widget {
+
/// Main trait defining a `View` behaviour, which needs be implemented in order to
+
/// build a custom widget. A `View` operates on an application state and can emit
+
/// application messages. It's usually is accompanied by a definition of view-specific
+
/// properties, which are being built from the application state by the framework.
+
pub trait View {
    type State;
    type Message;

-
    /// Should return a new view with props build from state (if type is known) and a
-
    /// message sender set.
-
    fn new(state: &Self::State, tx: UnboundedSender<Self::Message>) -> Self
-
    where
-
        Self: Sized;
-

    /// Should handle key events and call `handle_event` on all children.
-
    ///
-
    /// After key events have been handled, the custom event handler `on_event` should
-
    /// be called
-
    fn handle_event(&mut self, key: Key);
+
    fn handle_event(&mut self, key: Key) -> Option<Self::Message>;

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

+
    /// 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
+
    }
+
}
+

+
/// A `View` needs to wrapped into a `Widget` before being able to use with the
+
/// framework. A `Widget` enhances a `View` with event and update callbacks and takes
+
/// care of calling them before / after calling into the `View`.
+
pub struct Widget<S, M> {
+
    view: BoxedView<S, M>,
+
    sender: UnboundedSender<M>,
+
    on_update: Option<UpdateCallback<S>>,
+
    on_event: Option<EventCallback<M>>,
+
}
+

+
impl<S: 'static, M: 'static> Widget<S, M> {
+
    pub fn new<V>(view: V, sender: UnboundedSender<M>) -> Self
+
    where
+
        Self: Sized,
+
        V: View<State = S, Message = M> + 'static,
+
    {
+
        Self {
+
            view: Box::new(view),
+
            sender: sender.clone(),
+
            on_update: None,
+
            on_event: None,
+
        }
+
    }
+

+
    /// 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) {
+
            let _ = self.sender.send(message);
+
        }
+

+
        if let Some(on_event) = self.on_event {
+
            if let Some(message) = (on_event)(self.view.view_state().as_ref(), key) {
+
                let _ = self.sender.send(message);
+
            }
+
        }
+
    }
+

    /// Applications are usually defined by app-specific widgets that do know
    /// the type of `state`. These can use widgets from the library that do not know the
    /// type of `state`.
@@ -111,88 +173,58 @@ pub trait Widget {
    /// construct and update the internal props. If it is not set, app widgets can construct
    /// props directly via their state converters, whereas library widgets can just fallback
    /// to their current props.
-
    fn update(&mut self, state: &Self::State);
-

-
    /// Renders a widget to the given frame in the given area.
-
    ///
-
    /// Optional render props can be given.
-
    fn render(&self, frame: &mut Frame, props: RenderProps);
-

-
    /// Return a reference to this widgets' base.
-
    fn base(&self) -> &WidgetBase<Self::State, Self::Message>;
-

-
    /// Return a mutable reference to this widgets' base.
-
    fn base_mut(&mut self) -> &mut WidgetBase<Self::State, Self::Message>;
+
    pub fn update(&mut self, state: &S) {
+
        let props = self.on_update.map(|on_update| (on_update)(state));
+
        self.view.update(state, props);
+
    }

-
    /// Send a message to the widgets' base channel.
-
    fn send(&self, message: Self::Message) -> Result<(), SendError<Self::Message>> {
-
        self.base().send(message)
+
    /// Renders the wrapped view.
+
    pub fn render(&self, frame: &mut Frame, props: RenderProps) {
+
        self.view.render(frame, props);
    }

-
    /// Should set the optional custom event handler.
-
    fn on_event(mut self, callback: EventCallback) -> Self
+
    /// Sets the optional custom event handler.
+
    pub fn on_event(mut self, callback: EventCallback<M>) -> Self
    where
        Self: Sized,
    {
-
        self.base_mut().on_event = Some(callback);
+
        self.on_event = Some(callback);
        self
    }

-
    /// Should set the optional update handler.
-
    fn on_update(mut self, callback: UpdateCallback<Self::State>) -> Self
+
    /// Sets the optional update handler.
+
    pub fn on_update(mut self, callback: UpdateCallback<S>) -> Self
    where
        Self: Sized,
    {
-
        self.base_mut().on_update = Some(callback);
+
        self.on_update = Some(callback);
        self
    }

-
    /// Returns a boxed `Widget`
-
    fn to_boxed(self) -> Box<Self>
-
    where
-
        Self: Sized,
-
    {
-
        Box::new(self)
+
    /// Sends a message to the widgets' message channel.
+
    pub fn send(&self, message: M) {
+
        let _ = self.sender.send(message);
    }
}

-
/// Needs to be implemented for items that are supposed to be rendered in tables.
-
pub trait ToRow<const W: usize> {
-
    fn to_row(&self) -> [Cell; W];
-
}
-

-
/// Common trait for widget properties.
-
pub trait Properties {
-
    fn to_boxed(self) -> Box<Self>
-
    where
-
        Self: Sized,
-
    {
-
        Box::new(self)
-
    }
-

-
    fn from_callback<S>(callback: Option<UpdateCallback<S>>, state: &S) -> Option<Self>
+
/// A `View` needs to be wrapped into a `Widget` in order to be used with the framework.
+
/// `ToWidget` provides a blanket implementation for all `View`s.
+
pub trait ToWidget<S, M> {
+
    fn to_widget(self, tx: UnboundedSender<M>) -> Widget<S, M>
    where
-
        Self: Sized + Clone + 'static + BoxedAny,
-
    {
-
        callback
-
            .map(|callback| (callback)(state))
-
            .and_then(|props| Self::from_boxed_any(props))
-
    }
+
        Self: Sized + 'static;
}

-
/// Provide default implementations for conversions to and from `Box<dyn Any>`.
-
pub trait BoxedAny {
-
    fn from_boxed_any(any: Box<dyn Any>) -> Option<Self>
-
    where
-
        Self: Sized + Clone + 'static,
-
    {
-
        any.downcast_ref::<Self>().cloned()
-
    }
-

-
    fn to_boxed_any(self) -> Box<dyn Any>
+
impl<T, S, M> ToWidget<S, M> for T
+
where
+
    T: View<State = S, Message = M>,
+
    S: 'static,
+
    M: 'static,
+
{
+
    fn to_widget(self, tx: UnboundedSender<M>) -> Widget<S, M>
    where
-
        Self: Sized + Clone + 'static,
+
        Self: Sized + 'static,
    {
-
        Box::new(self)
+
        Widget::new(self, tx)
    }
}
modified src/ui/widget/container.rs
@@ -1,6 +1,5 @@
use std::fmt::Debug;
-

-
use tokio::sync::mpsc::UnboundedSender;
+
use std::marker::PhantomData;

use termion::event::Key;

@@ -10,7 +9,7 @@ use ratatui::widgets::{Block, BorderType, Borders, Row};
use crate::ui::ext::{FooterBlock, FooterBlockType, HeaderBlock};
use crate::ui::theme::style;

-
use super::{BoxedAny, BoxedWidget, Properties, RenderProps, Widget, WidgetBase};
+
use super::{RenderProps, View, ViewProps, Widget};

#[derive(Clone, Debug)]
pub struct Column<'a> {
@@ -64,14 +63,20 @@ impl<'a> Default for HeaderProps<'a> {
    }
}

-
impl<'a: 'static> Properties for HeaderProps<'a> {}
-
impl<'a: 'static> BoxedAny for HeaderProps<'a> {}
-

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

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

impl<'a, S, A> Header<'a, S, A> {
@@ -87,25 +92,18 @@ impl<'a, S, A> Header<'a, S, A> {
    }
}

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

-
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self {
-
        Self {
-
            base: WidgetBase::new(tx.clone()),
-
            props: HeaderProps::default(),
-
        }
+
    fn handle_event(&mut self, _key: Key) -> Option<Self::Message> {
+
        None
    }

-
    fn handle_event(&mut self, _key: Key) {}
-

-
    fn update(&mut self, state: &S) {
-
        self.props = self
-
            .base
-
            .on_update
-
            .and_then(|on_update| HeaderProps::from_boxed_any((on_update)(state)))
-
            .unwrap_or(self.props.clone());
+
    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, frame: &mut ratatui::Frame, props: RenderProps) {
@@ -165,14 +163,6 @@ impl<'a: 'static, S, M> Widget for Header<'a, S, M> {
        frame.render_widget(block, props.area);
        frame.render_widget(header, header_layout[0]);
    }
-

-
    fn base(&self) -> &WidgetBase<S, M> {
-
        &self.base
-
    }
-

-
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
-
        &mut self.base
-
    }
}

#[derive(Clone, Debug)]
@@ -205,14 +195,20 @@ impl<'a> Default for FooterProps<'a> {
    }
}

-
impl<'a: 'static> Properties for FooterProps<'a> {}
-
impl<'a: 'static> BoxedAny for FooterProps<'a> {}
-

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

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

impl<'a, S, M> Footer<'a, S, M> {
@@ -250,25 +246,18 @@ impl<'a, S, M> Footer<'a, S, M> {
    }
}

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

-
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self {
-
        Self {
-
            base: WidgetBase::new(tx.clone()),
-
            props: FooterProps::default(),
-
        }
+
    fn handle_event(&mut self, _key: Key) -> Option<Self::Message> {
+
        None
    }

-
    fn handle_event(&mut self, _key: Key) {}
-

-
    fn update(&mut self, state: &S) {
-
        self.props = self
-
            .base
-
            .on_update
-
            .and_then(|on_update| FooterProps::from_boxed_any((on_update)(state)))
-
            .unwrap_or(self.props.clone());
+
    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, frame: &mut ratatui::Frame, props: RenderProps) {
@@ -304,14 +293,6 @@ impl<'a: 'static, S, M> Widget for Footer<'a, S, M> {
            self.render_cell(frame, *area, block_type, cell.clone(), props.focus);
        }
    }
-

-
    fn base(&self) -> &WidgetBase<S, M> {
-
        &self.base
-
    }
-

-
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
-
        &mut self.base
-
    }
}

#[derive(Clone, Default)]
@@ -326,65 +307,65 @@ impl ContainerProps {
    }
}

-
impl Properties for ContainerProps {}
-
impl BoxedAny for ContainerProps {}
-

pub struct Container<S, M> {
-
    /// Internal base
-
    base: WidgetBase<S, M>,
    /// Internal props
    props: ContainerProps,
    /// Container header
-
    header: Option<BoxedWidget<S, M>>,
+
    header: Option<Widget<S, M>>,
    /// Content widget
-
    content: Option<BoxedWidget<S, M>>,
+
    content: Option<Widget<S, M>>,
    /// Container footer
-
    footer: Option<BoxedWidget<S, M>>,
+
    footer: Option<Widget<S, M>>,
+
}
+

+
impl<S, M> Default for Container<S, M> {
+
    fn default() -> Self {
+
        Self {
+
            props: ContainerProps::default(),
+
            header: None,
+
            content: None,
+
            footer: None,
+
        }
+
    }
}

impl<S, M> Container<S, M> {
-
    pub fn header(mut self, header: BoxedWidget<S, M>) -> Self {
+
    pub fn header(mut self, header: Widget<S, M>) -> Self {
        self.header = Some(header);
        self
    }

-
    pub fn content(mut self, content: BoxedWidget<S, M>) -> Self {
+
    pub fn content(mut self, content: Widget<S, M>) -> Self {
        self.content = Some(content);
        self
    }

-
    pub fn footer(mut self, footer: BoxedWidget<S, M>) -> Self {
+
    pub fn footer(mut self, footer: Widget<S, M>) -> Self {
        self.footer = Some(footer);
        self
    }
}

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

-
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        Self {
-
            base: WidgetBase::new(tx.clone()),
-
            props: ContainerProps::default(),
-
            header: None,
-
            content: None,
-
            footer: None,
-
        }
-
    }
-

-
    fn handle_event(&mut self, key: termion::event::Key) {
+
    fn handle_event(&mut self, key: termion::event::Key) -> Option<Self::Message> {
        if let Some(content) = &mut self.content {
            content.handle_event(key);
        }
+

+
        None
    }

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

        if let Some(header) = &mut self.header {
            header.update(state);
@@ -445,14 +426,6 @@ impl<S, M> Widget for Container<S, M> {
            footer.render(frame, RenderProps::from(footer_area).focus(props.focus));
        }
    }
-

-
    fn base(&self) -> &WidgetBase<S, M> {
-
        &self.base
-
    }
-

-
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
-
        &mut self.base
-
    }
}

#[derive(Clone)]
@@ -461,8 +434,6 @@ pub struct SectionGroupState {
    focus: Option<usize>,
}

-
impl BoxedAny for SectionGroupState {}
-

#[derive(Clone, Default)]
pub struct SectionGroupProps {
    /// If this pages' keys should be handled.
@@ -476,22 +447,27 @@ impl SectionGroupProps {
    }
}

-
impl Properties for SectionGroupProps {}
-
impl BoxedAny for SectionGroupProps {}
-

pub struct SectionGroup<S, M> {
-
    /// Internal base
-
    base: WidgetBase<S, M>,
    /// Internal table properties
    props: SectionGroupProps,
    /// All sections
-
    sections: Vec<BoxedWidget<S, M>>,
+
    sections: Vec<Widget<S, M>>,
    /// Internal selection and offset state
    state: SectionGroupState,
}

+
impl<S, M> Default for SectionGroup<S, M> {
+
    fn default() -> Self {
+
        Self {
+
            props: SectionGroupProps::default(),
+
            sections: vec![],
+
            state: SectionGroupState { focus: Some(0) },
+
        }
+
    }
+
}
+

impl<S, M> SectionGroup<S, M> {
-
    pub fn section(mut self, section: BoxedWidget<S, M>) -> Self {
+
    pub fn section(mut self, section: Widget<S, M>) -> Self {
        self.sections.push(section);
        self
    }
@@ -515,7 +491,7 @@ impl<S, M> SectionGroup<S, M> {
    }
}

-
impl<S, M> Widget for SectionGroup<S, M>
+
impl<S, M> View for SectionGroup<S, M>
where
    S: 'static,
    M: 'static,
@@ -523,16 +499,7 @@ where
    type State = S;
    type Message = M;

-
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self {
-
        Self {
-
            base: WidgetBase::new(tx.clone()),
-
            props: SectionGroupProps::default(),
-
            sections: vec![],
-
            state: SectionGroupState { focus: Some(0) },
-
        }
-
    }
-

-
    fn handle_event(&mut self, key: Key) {
+
    fn handle_event(&mut self, key: Key) -> Option<Self::Message> {
        if let Some(section) = self
            .state
            .focus
@@ -553,14 +520,13 @@ where
            }
        }

-
        if let Some(on_event) = self.base.on_event {
-
            (on_event)(self, key);
-
        }
+
        None
    }

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

        for section in &mut self.sections {
            section.update(state);
@@ -582,12 +548,4 @@ where
            }
        }
    }
-

-
    fn base(&self) -> &WidgetBase<S, M> {
-
        &self.base
-
    }
-

-
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
-
        &mut self.base
-
    }
}
modified src/ui/widget/input.rs
@@ -1,12 +1,12 @@
-
use termion::event::Key;
+
use std::marker::PhantomData;

-
use tokio::sync::mpsc::UnboundedSender;
+
use termion::event::Key;

use ratatui::layout::{Constraint, Layout};
use ratatui::style::Stylize;
use ratatui::text::{Line, Span};

-
use super::{BoxedAny, Properties, RenderProps, Widget, WidgetBase};
+
use super::{RenderProps, View, ViewProps, ViewState};

#[derive(Clone)]
pub struct TextFieldProps {
@@ -46,30 +46,35 @@ impl Default for TextFieldProps {
    }
}

-
impl Properties for TextFieldProps {}
-

#[derive(Clone)]
struct TextFieldState {
    pub text: Option<String>,
    pub cursor_position: usize,
}

-
impl BoxedAny for TextFieldState {}
-

pub struct TextField<S, M> {
-
    /// Internal base
-
    base: WidgetBase<S, M>,
    /// Internal props
    props: TextFieldProps,
    /// Internal state
    state: TextFieldState,
+
    /// Phantom
+
    phantom: PhantomData<(S, M)>,
}

-
impl<S, M> TextField<S, M> {
-
    pub fn text(&self) -> Option<&String> {
-
        self.state.text.as_ref()
+
impl<S, M> Default for TextField<S, M> {
+
    fn default() -> Self {
+
        Self {
+
            props: TextFieldProps::default(),
+
            state: TextFieldState {
+
                text: None,
+
                cursor_position: 0,
+
            },
+
            phantom: PhantomData,
+
        }
    }
+
}

+
impl<S, M> TextField<S, M> {
    fn move_cursor_left(&mut self) {
        let cursor_moved_left = self.state.cursor_position.saturating_sub(1);
        self.state.cursor_position = self.clamp_cursor(cursor_moved_left);
@@ -131,7 +136,7 @@ impl<S, M> TextField<S, M> {
    }
}

-
impl<S, M> Widget for TextField<S, M>
+
impl<S, M> View for TextField<S, M>
where
    S: 'static,
    M: 'static,
@@ -139,18 +144,7 @@ where
    type Message = M;
    type State = S;

-
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self {
-
        Self {
-
            base: WidgetBase::new(tx.clone()),
-
            props: TextFieldProps::default(),
-
            state: TextFieldState {
-
                text: None,
-
                cursor_position: 0,
-
            },
-
        }
-
    }
-

-
    fn handle_event(&mut self, key: Key) {
+
    fn handle_event(&mut self, key: Key) -> Option<Self::Message> {
        match key {
            Key::Char(to_insert)
                if (key != Key::Alt('\n'))
@@ -171,21 +165,17 @@ where
            _ => {}
        }

-
        if let Some(on_event) = self.base.on_event {
-
            (on_event)(self, key);
-
        }
+
        None
    }

-
    fn update(&mut self, state: &S) {
-
        if let Some(on_update) = self.base.on_update {
-
            if let Some(props) = (on_update)(state).downcast_ref::<TextFieldProps>() {
-
                self.props = props.clone();
+
    fn update(&mut self, _state: &Self::State, props: Option<ViewProps>) {
+
        if let Some(props) = props.and_then(|props| props.inner::<TextFieldProps>()) {
+
            self.props = props;

-
                if self.state.text.is_none() {
-
                    self.state.cursor_position = props.text.len().saturating_sub(1);
-
                }
-
                self.state.text = Some(props.text.clone());
+
            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());
        }
    }

@@ -238,13 +228,10 @@ where
        }
    }

-
    fn base(&self) -> &WidgetBase<S, M> {
-
        &self.base
-
    }
-

-
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
-
        &mut self.base
+
    fn view_state(&self) -> Option<ViewState> {
+
        self.state
+
            .text
+
            .as_ref()
+
            .map(|text| ViewState::String(text.to_string()))
    }
}
-

-
impl<S, M> BoxedAny for TextField<S, M> {}
modified src/ui/widget/list.rs
@@ -1,8 +1,7 @@
use std::cmp;
+
use std::marker::PhantomData;

use ratatui::widgets::Row;
-
use tokio::sync::mpsc::UnboundedSender;
-

use termion::event::Key;

use ratatui::layout::Constraint;
@@ -10,11 +9,12 @@ use ratatui::style::Stylize;
use ratatui::text::Text;
use ratatui::widgets::TableState;

+
use crate::ui::items::ToRow;
use crate::ui::theme::style;
use crate::ui::{layout, span};

-
use super::BoxedAny;
-
use super::{container::Column, Properties, RenderProps, ToRow, Widget, WidgetBase};
+
use super::{container::Column, RenderProps, View};
+
use super::{ViewProps, ViewState};

#[derive(Clone, Debug)]
pub struct TableProps<'a, R, const W: usize>
@@ -83,31 +83,35 @@ where
    }
}

-
impl<'a: 'static, R, const W: usize> Properties for TableProps<'a, R, W> where R: ToRow<W> + 'static {}
-
impl<'a: 'static, R, const W: usize> BoxedAny for TableProps<'a, R, W> where R: ToRow<W> + 'static {}
-

-
impl BoxedAny for TableState {}
-

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

-
impl<'a, S, M, R, const W: usize> Table<'a, S, M, R, W>
+
impl<'a, S, M, R, const W: usize> Default for Table<'a, S, M, R, W>
where
    R: ToRow<W>,
{
-
    pub fn selected(&self) -> Option<usize> {
-
        self.state.selected()
+
    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>
+
where
+
    R: ToRow<W>,
+
{
    fn prev(&mut self) -> Option<usize> {
        let selected = self
            .state
@@ -159,22 +163,14 @@ where
    }
}

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

-
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self {
-
        Self {
-
            base: WidgetBase::new(tx.clone()),
-
            props: TableProps::default(),
-
            state: TableState::default().with_selected(Some(0)),
-
        }
-
    }
-

-
    fn handle_event(&mut self, key: Key) {
+
    fn handle_event(&mut self, key: Key) -> Option<Self::Message> {
        match key {
            Key::Up | Key::Char('k') => {
                self.prev();
@@ -197,18 +193,17 @@ where
            _ => {}
        }

-
        if let Some(on_event) = self.base.on_event {
-
            (on_event)(self, key);
-
        }
+
        None
    }

-
    fn update(&mut self, state: &S) {
-
        self.props =
-
            TableProps::from_callback(self.base.on_update, state).unwrap_or(self.props.clone());
-

-
        if self.props.selected != self.state.selected() {
-
            self.state.select(self.props.selected);
+
    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;
        }
+

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

    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
@@ -268,12 +263,8 @@ where
        }
    }

-
    fn base(&self) -> &WidgetBase<S, M> {
-
        &self.base
-
    }
-

-
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
-
        &mut self.base
+
    fn view_state(&self) -> Option<ViewState> {
+
        self.state.selected().map(ViewState::USize)
    }
}

modified src/ui/widget/text.rs
@@ -1,11 +1,11 @@
-
use tokio::sync::mpsc::UnboundedSender;
+
use std::marker::PhantomData;

use termion::event::Key;

use ratatui::layout::{Constraint, Layout};
use ratatui::text::Text;

-
use super::{BoxedAny, Properties, RenderProps, Widget, WidgetBase};
+
use super::{RenderProps, View, ViewProps, ViewState};

#[derive(Clone)]
pub struct ParagraphProps<'a> {
@@ -40,9 +40,6 @@ impl<'a> Default for ParagraphProps<'a> {
    }
}

-
impl<'a: 'static> Properties for ParagraphProps<'a> {}
-
impl<'a: 'static> BoxedAny for ParagraphProps<'a> {}
-

#[derive(Clone)]
struct ParagraphState {
    /// Internal offset
@@ -51,15 +48,26 @@ struct ParagraphState {
    pub progress: usize,
}

-
impl BoxedAny for ParagraphState {}
-

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

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

impl<'a, S, M> Paragraph<'a, S, M> {
@@ -118,10 +126,6 @@ impl<'a, S, M> Paragraph<'a, S, M> {
        self.scroll()
    }

-
    pub fn progress(&self) -> usize {
-
        self.state.progress
-
    }
-

    fn scroll_percent(offset: usize, len: usize, height: usize) -> usize {
        if height >= len {
            100
@@ -136,7 +140,7 @@ impl<'a, S, M> Paragraph<'a, S, M> {
    }
}

-
impl<'a, S, M> Widget for Paragraph<'a, S, M>
+
impl<'a, S, M> View for Paragraph<'a, S, M>
where
    'a: 'static,
    S: 'static,
@@ -145,21 +149,7 @@ where
    type Message = M;
    type State = S;

-
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        Self {
-
            base: WidgetBase::new(tx.clone()),
-
            props: ParagraphProps::default(),
-
            state: ParagraphState {
-
                offset: 0,
-
                progress: 0,
-
            },
-
        }
-
    }
-

-
    fn handle_event(&mut self, key: Key) {
+
    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;

@@ -185,14 +175,13 @@ where
            _ => {}
        }

-
        if let Some(on_event) = self.base.on_event {
-
            (on_event)(self, key);
-
        }
+
        None
    }

-
    fn update(&mut self, state: &S) {
-
        self.props =
-
            ParagraphProps::from_callback(self.base.on_update, state).unwrap_or(self.props.clone());
+
    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, frame: &mut ratatui::Frame, props: RenderProps) {
@@ -205,11 +194,7 @@ where
        frame.render_widget(content, content_area);
    }

-
    fn base(&self) -> &WidgetBase<S, M> {
-
        &self.base
-
    }
-

-
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
-
        &mut self.base
+
    fn view_state(&self) -> Option<ViewState> {
+
        Some(ViewState::USize(self.state.progress))
    }
}
modified src/ui/widget/window.rs
@@ -1,7 +1,5 @@
-
use std::collections::HashMap;
use std::hash::Hash;
-

-
use tokio::sync::mpsc::UnboundedSender;
+
use std::{collections::HashMap, marker::PhantomData};

use termion::event::Key;

@@ -12,7 +10,7 @@ use ratatui::widgets::Row;

use crate::ui::theme::style;

-
use super::{BoxedAny, BoxedWidget, Properties, RenderProps, Widget, WidgetBase};
+
use super::{RenderProps, View, ViewProps, Widget};

#[derive(Clone)]
pub struct WindowProps<Id> {
@@ -32,29 +30,33 @@ impl<Id> Default for WindowProps<Id> {
    }
}

-
impl<Id> Properties for WindowProps<Id> {}
-
impl<Id> BoxedAny for WindowProps<Id> {}
-

pub struct Window<S, M, Id> {
-
    /// Internal base
-
    base: WidgetBase<S, M>,
    /// Internal properties
    props: WindowProps<Id>,
    /// All pages known
-
    pages: HashMap<Id, BoxedWidget<S, M>>,
+
    pages: HashMap<Id, Widget<S, M>>,
+
}
+

+
impl<S, M, Id> Default for Window<S, M, Id> {
+
    fn default() -> Self {
+
        Self {
+
            props: WindowProps::default(),
+
            pages: HashMap::new(),
+
        }
+
    }
}

impl<S, M, Id> Window<S, M, Id>
where
    Id: Clone + Hash + Eq + PartialEq,
{
-
    pub fn page(mut self, id: Id, page: BoxedWidget<S, M>) -> Self {
+
    pub fn page(mut self, id: Id, page: Widget<S, M>) -> Self {
        self.pages.insert(id, page);
        self
    }
}

-
impl<'a, S, M, Id> Widget for Window<S, M, Id>
+
impl<'a, S, M, Id> View for Window<S, M, Id>
where
    'a: 'static,
    S: 'static,
@@ -64,18 +66,7 @@ where
    type Message = M;
    type State = S;

-
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        Self {
-
            base: WidgetBase::new(tx.clone()),
-
            props: WindowProps::default(),
-
            pages: HashMap::new(),
-
        }
-
    }
-

-
    fn handle_event(&mut self, key: termion::event::Key) {
+
    fn handle_event(&mut self, key: termion::event::Key) -> Option<Self::Message> {
        let page = self
            .props
            .current_page
@@ -86,14 +77,13 @@ where
            page.handle_event(key);
        }

-
        if let Some(on_event) = self.base.on_event {
-
            (on_event)(self, key);
-
        }
+
        None
    }

-
    fn update(&mut self, state: &S) {
-
        self.props =
-
            WindowProps::from_callback(self.base.on_update, state).unwrap_or(self.props.clone());
+
    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;
+
        }

        let page = self
            .props
@@ -119,14 +109,6 @@ where
            page.render(frame, RenderProps::from(area).focus(true));
        }
    }
-

-
    fn base(&self) -> &WidgetBase<S, M> {
-
        &self.base
-
    }
-

-
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
-
        &mut self.base
-
    }
}

#[derive(Clone)]
@@ -159,14 +141,11 @@ impl Default for ShortcutsProps {
    }
}

-
impl Properties for ShortcutsProps {}
-
impl BoxedAny for ShortcutsProps {}
-

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

impl<S, M> Shortcuts<S, M> {
@@ -186,22 +165,27 @@ impl<S, M> Shortcuts<S, M> {
    }
}

-
impl<S, M> Widget for Shortcuts<S, M> {
-
    type Message = M;
-
    type State = S;
-

-
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self {
+
impl<S, M> Default for Shortcuts<S, M> {
+
    fn default() -> Self {
        Self {
-
            base: WidgetBase::new(tx.clone()),
            props: ShortcutsProps::default(),
+
            phantom: PhantomData,
        }
    }
+
}

-
    fn handle_event(&mut self, _key: Key) {}
+
impl<S, M> View for Shortcuts<S, M> {
+
    type Message = M;
+
    type State = S;

-
    fn update(&mut self, state: &S) {
-
        self.props =
-
            ShortcutsProps::from_callback(self.base.on_update, state).unwrap_or(self.props.clone());
+
    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) {
@@ -241,12 +225,4 @@ impl<S, M> Widget for Shortcuts<S, M> {
        let table = Table::new([Row::new(row)], widths).column_spacing(0);
        frame.render_widget(table, props.area);
    }
-

-
    fn base(&self) -> &WidgetBase<S, M> {
-
        &self.base
-
    }
-

-
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
-
        &mut self.base
-
    }
}