Basic states

This commit is contained in:
ClementTsang 2021-12-24 16:37:05 -05:00
parent c1bbe7627d
commit b304db6a2f
18 changed files with 307 additions and 176 deletions

View File

@ -256,7 +256,7 @@ impl Application for AppState {
.into()
}
fn destroy(&mut self) {
fn destructor(&mut self) {
// TODO: Eventually move some thread logic into the app creation, and destroy here?
}

View File

@ -24,7 +24,7 @@ pub trait Application: Sized {
fn view<'b>(&mut self, ctx: &mut ViewContext<'_>) -> Element<'static, Self::Message>;
/// To run upon stopping the application.
fn destroy(&mut self) {}
fn destructor(&mut self) {}
/// An optional event handler, intended for use with global shortcuts or events.
/// This will be run *after* trying to send the events into the user interface, and

View File

@ -5,23 +5,28 @@ pub mod widget;
pub use widget::*;
use enum_dispatch::enum_dispatch;
use tui::{layout::Rect, Frame};
use tui::Frame;
use super::{Bounds, DrawContext, Event, LayoutNode, Size, Status};
use super::{Bounds, DrawContext, Event, LayoutNode, Size, StateContext, Status};
/// A component displays information and can be interacted with.
#[allow(unused_variables)]
#[enum_dispatch]
pub trait TmpComponent<Message> {
/// Draws the component.
fn draw<Backend>(&mut self, context: DrawContext<'_>, frame: &mut Frame<'_, Backend>)
where
fn draw<Backend>(
&mut self, state_ctx: &mut StateContext<'_>, draw_ctx: DrawContext<'_>,
frame: &mut Frame<'_, Backend>,
) where
Backend: tui::backend::Backend;
/// How a component should react to an [`Event`].
///
/// Defaults to just ignoring the event.
fn on_event(&mut self, area: Rect, event: Event, messages: &mut Vec<Message>) -> Status {
fn on_event(
&mut self, state_ctx: &mut StateContext<'_>, draw_ctx: DrawContext<'_>, event: Event,
messages: &mut Vec<Message>,
) -> Status {
Status::Ignored
}

View File

@ -1,18 +1,23 @@
use tui::{backend::Backend, layout::Rect, Frame};
use tui::{backend::Backend, Frame};
use crate::tuine::{DrawContext, Event, Status, TmpComponent};
use crate::tuine::{DrawContext, Event, StateContext, Status, TmpComponent};
pub struct Block {}
impl<Message> TmpComponent<Message> for Block {
fn draw<B>(&mut self, _context: DrawContext<'_>, _frame: &mut Frame<'_, B>)
where
fn draw<B>(
&mut self, _state_ctx: &mut StateContext<'_>, _draw_ctx: DrawContext<'_>,
_frame: &mut Frame<'_, B>,
) where
B: Backend,
{
todo!()
}
fn on_event(&mut self, _area: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
fn on_event(
&mut self, _state_ctx: &mut StateContext<'_>, _draw_ctx: DrawContext<'_>, _event: Event,
_messages: &mut Vec<Message>,
) -> Status {
Status::Ignored
}
}

View File

@ -1,18 +1,23 @@
use tui::{backend::Backend, layout::Rect, Frame};
use tui::{backend::Backend, Frame};
use crate::tuine::{DrawContext, Event, Status, TmpComponent};
use crate::tuine::{DrawContext, Event, StateContext, Status, TmpComponent};
pub struct Carousel {}
impl<Message> TmpComponent<Message> for Carousel {
fn draw<B>(&mut self, _context: DrawContext<'_>, _frame: &mut Frame<'_, B>)
where
fn draw<B>(
&mut self, _state_ctx: &mut StateContext<'_>, _draw_ctx: DrawContext<'_>,
_frame: &mut Frame<'_, B>,
) where
B: Backend,
{
todo!()
}
fn on_event(&mut self, _area: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
fn on_event(
&mut self, _state_ctx: &mut StateContext<'_>, _draw_ctx: DrawContext<'_>, _event: Event,
_messages: &mut Vec<Message>,
) -> Status {
Status::Ignored
}
}

View File

@ -1,6 +1,8 @@
use tui::{backend::Backend, layout::Rect, Frame};
use tui::{backend::Backend, Frame};
use crate::tuine::{Bounds, DrawContext, Element, Event, LayoutNode, Size, Status, TmpComponent};
use crate::tuine::{
Bounds, DrawContext, Element, Event, LayoutNode, Size, StateContext, Status, TmpComponent,
};
/// A [`Container`] just contains a child, as well as being able to be sized.
///
@ -38,14 +40,19 @@ impl<'a, Message> Container<'a, Message> {
}
impl<'a, Message> TmpComponent<Message> for Container<'a, Message> {
fn draw<B>(&mut self, context: DrawContext<'_>, _frame: &mut Frame<'_, B>)
where
fn draw<B>(
&mut self, _state_ctx: &mut StateContext<'_>, _draw_ctx: DrawContext<'_>,
_frame: &mut Frame<'_, B>,
) where
B: Backend,
{
todo!()
}
fn on_event(&mut self, _area: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
fn on_event(
&mut self, _state_ctx: &mut StateContext<'_>, _draw_ctx: DrawContext<'_>, _event: Event,
_messages: &mut Vec<Message>,
) -> Status {
todo!()
}

View File

@ -3,7 +3,9 @@ use tui::{backend::Backend, layout::Rect, Frame};
pub mod flex_element;
pub use flex_element::FlexElement;
use crate::tuine::{Bounds, DrawContext, Element, Event, LayoutNode, Size, Status, TmpComponent};
use crate::tuine::{
Bounds, DrawContext, Element, Event, LayoutNode, Size, StateContext, Status, TmpComponent,
};
#[derive(Clone, Copy, Debug)]
pub enum Axis {
@ -84,21 +86,26 @@ impl<'a, Message> Flex<'a, Message> {
}
impl<'a, Message> TmpComponent<Message> for Flex<'a, Message> {
fn draw<B>(&mut self, context: DrawContext<'_>, frame: &mut Frame<'_, B>)
where
fn draw<B>(
&mut self, state_ctx: &mut StateContext<'_>, draw_ctx: DrawContext<'_>,
frame: &mut Frame<'_, B>,
) where
B: Backend,
{
self.children
.iter_mut()
.zip(context.children())
.zip(draw_ctx.children())
.for_each(|(child, child_node)| {
if child_node.should_draw() {
child.draw(child_node, frame);
child.draw(state_ctx, child_node, frame);
}
});
}
fn on_event(&mut self, area: Rect, event: Event, messages: &mut Vec<Message>) -> Status {
fn on_event(
&mut self, _state_ctx: &mut StateContext<'_>, _draw_ctx: DrawContext<'_>, event: Event,
messages: &mut Vec<Message>,
) -> Status {
// FIXME: On event for flex
Status::Ignored

View File

@ -1,6 +1,8 @@
use tui::{backend::Backend, layout::Rect, Frame};
use tui::{backend::Backend, Frame};
use crate::tuine::{Bounds, DrawContext, Element, Event, LayoutNode, Size, Status, TmpComponent};
use crate::tuine::{
Bounds, DrawContext, Element, Event, LayoutNode, Size, StateContext, Status, TmpComponent,
};
use super::Axis;
@ -37,17 +39,20 @@ impl<'a, Message> FlexElement<'a, Message> {
self
}
pub(crate) fn draw<B>(&mut self, context: DrawContext<'_>, frame: &mut Frame<'_, B>)
where
pub(crate) fn draw<B>(
&mut self, state_ctx: &mut StateContext<'_>, draw_ctx: DrawContext<'_>,
frame: &mut Frame<'_, B>,
) where
B: Backend,
{
self.element.draw(context, frame)
self.element.draw(state_ctx, draw_ctx, frame)
}
pub(crate) fn on_event(
&mut self, area: Rect, event: Event, messages: &mut Vec<Message>,
&mut self, state_ctx: &mut StateContext<'_>, draw_ctx: DrawContext<'_>, event: Event,
messages: &mut Vec<Message>,
) -> Status {
self.element.on_event(area, event, messages)
self.element.on_event(state_ctx, draw_ctx, event, messages)
}
/// Assumes the flex is 0. Just calls layout on its child.

View File

@ -1,6 +1,6 @@
use tui::{backend::Backend, layout::Rect, Frame};
use tui::{backend::Backend, Frame};
use crate::tuine::{DrawContext, Event, Status, TmpComponent};
use crate::tuine::{DrawContext, Event, StateContext, Status, TmpComponent};
/// A [`Component`] to handle keyboard shortcuts and assign actions to them.
///
@ -8,14 +8,19 @@ use crate::tuine::{DrawContext, Event, Status, TmpComponent};
pub struct Shortcut {}
impl<Message> TmpComponent<Message> for Shortcut {
fn draw<B>(&mut self, _context: DrawContext<'_>, _frame: &mut Frame<'_, B>)
where
fn draw<B>(
&mut self, _state_ctx: &mut StateContext<'_>, _draw_ctx: DrawContext<'_>,
_frame: &mut Frame<'_, B>,
) where
B: Backend,
{
todo!()
}
fn on_event(&mut self, _area: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
fn on_event(
&mut self, _state_ctx: &mut StateContext<'_>, _draw_ctx: DrawContext<'_>, _event: Event,
_messages: &mut Vec<Message>,
) -> Status {
Status::Ignored
}
}

View File

@ -14,7 +14,7 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::{
constants::TABLE_GAP_HEIGHT_LIMIT,
tuine::{DrawContext, Event, Status, TmpComponent, ViewContext},
tuine::{DrawContext, Event, Key, StateContext, Status, TmpComponent, ViewContext},
};
pub use self::table_column::{TextColumn, TextColumnConstraint};
@ -29,7 +29,7 @@ pub struct StyleSheet {
/// A sortable, scrollable table for text data.
pub struct TextTable<'a, Message> {
test_state: &'a mut TextTableState,
key: Key,
state: TextTableState,
column_widths: Vec<u16>,
columns: Vec<TextColumn>,
@ -46,10 +46,8 @@ pub struct TextTable<'a, Message> {
impl<'a, Message> TextTable<'a, Message> {
#[track_caller]
pub fn new<S: Into<Cow<'static, str>>>(ctx: &mut ViewContext<'_>, columns: Vec<S>) -> Self {
let test_state = ctx.state::<TextTableState>(Location::caller());
Self {
test_state,
key: ctx.register_component(Location::caller()),
state: TextTableState::default(),
column_widths: vec![0; columns.len()],
columns: columns
@ -169,11 +167,14 @@ impl<'a, Message> TextTable<'a, Message> {
}
impl<'a, Message> TmpComponent<Message> for TextTable<'a, Message> {
fn draw<B>(&mut self, context: DrawContext<'_>, frame: &mut Frame<'_, B>)
where
fn draw<B>(
&mut self, state_ctx: &mut StateContext<'_>, draw_ctx: DrawContext<'_>,
frame: &mut Frame<'_, B>,
) where
B: Backend,
{
let rect = context.rect();
let rect = draw_ctx.rect();
let state = state_ctx.mut_state::<TextTableState>(self.key);
self.table_gap = if !self.show_gap
|| (self.rows.len() + 2 > rect.height.into() && rect.height < TABLE_GAP_HEIGHT_LIMIT)
@ -198,10 +199,8 @@ impl<'a, Message> TmpComponent<Message> for TextTable<'a, Message> {
// as well as truncating some entries based on available width.
let data_slice = {
// Note: `get_list_start` already ensures `start` is within the bounds of the number of items, so no need to check!
let start = self
.state
.display_start_index(rect, scrollable_height as usize);
let end = min(self.state.num_items(), start + scrollable_height as usize);
let start = state.display_start_index(rect, scrollable_height as usize);
let end = min(state.num_items(), start + scrollable_height as usize);
self.rows[start..end].to_vec()
};
@ -219,13 +218,19 @@ impl<'a, Message> TmpComponent<Message> for TextTable<'a, Message> {
table = table.highlight_style(self.style_sheet.selected_text);
}
frame.render_stateful_widget(table.widths(&widths), rect, self.state.tui_state());
frame.render_stateful_widget(table.widths(&widths), rect, state.tui_state());
}
fn on_event(&mut self, area: Rect, event: Event, messages: &mut Vec<Message>) -> Status {
fn on_event(
&mut self, state_ctx: &mut StateContext<'_>, draw_ctx: DrawContext<'_>, event: Event,
messages: &mut Vec<Message>,
) -> Status {
use crate::tuine::MouseBoundIntersect;
use crossterm::event::{MouseButton, MouseEventKind};
let rect = draw_ctx.rect();
let state = state_ctx.mut_state::<TextTableState>(self.key);
match event {
Event::Keyboard(key_event) => {
if key_event.modifiers.is_empty() {
@ -237,10 +242,10 @@ impl<'a, Message> TmpComponent<Message> for TextTable<'a, Message> {
}
}
Event::Mouse(mouse_event) => {
if mouse_event.does_mouse_intersect_bounds(area) {
if mouse_event.does_mouse_intersect_bounds(rect) {
match mouse_event.kind {
MouseEventKind::Down(MouseButton::Left) => {
let y = mouse_event.row - area.top();
let y = mouse_event.row - rect.top();
if self.sortable && y == 0 {
todo!()

View File

@ -1,86 +0,0 @@
use std::{panic::Location, rc::Rc};
use rustc_hash::FxHashMap;
use tui::layout::Rect;
use super::{Key, LayoutNode, State};
#[derive(Default)]
pub struct StateMap(FxHashMap<Key, (Rc<Box<dyn State>>, bool)>);
impl StateMap {
pub fn state<S: State + Default + 'static>(&mut self, key: Key) -> Rc<Box<dyn State>> {
let state = self
.0
.entry(key)
.or_insert_with(|| (Rc::new(Box::new(S::default())), true));
state.1 = true;
state.0.clone()
}
}
pub struct ViewContext<'a> {
key_counter: usize,
state_map: &'a mut StateMap,
}
impl<'a> ViewContext<'a> {
pub fn new(state_map: &'a mut StateMap) -> Self {
Self {
key_counter: 0,
state_map,
}
}
pub fn state<S: State + Default + 'static>(
&mut self, location: &'static Location<'static>,
) -> Rc<Box<dyn State>> {
let key = Key::new(location, self.key_counter);
self.key_counter += 1;
self.state_map.state::<S>(key)
}
}
pub struct DrawContext<'a> {
current_node: &'a LayoutNode,
current_offset: (u16, u16),
}
impl<'a> DrawContext<'_> {
/// Creates a new [`DrawContext`], with the offset set to `(0, 0)`.
pub(crate) fn root(root: &'a LayoutNode) -> DrawContext<'a> {
DrawContext {
current_node: root,
current_offset: (0, 0),
}
}
pub(crate) fn rect(&self) -> Rect {
let mut rect = self.current_node.rect;
rect.x += self.current_offset.0;
rect.y += self.current_offset.1;
rect
}
pub(crate) fn should_draw(&self) -> bool {
self.current_node.rect.area() != 0
}
pub(crate) fn children(&self) -> impl Iterator<Item = DrawContext<'_>> {
let new_offset = (
self.current_offset.0 + self.current_node.rect.x,
self.current_offset.1 + self.current_node.rect.y,
);
self.current_node
.children
.iter()
.map(move |layout_node| DrawContext {
current_node: layout_node,
current_offset: new_offset,
})
}
}

View File

@ -0,0 +1,45 @@
use tui::layout::Rect;
use crate::tuine::LayoutNode;
pub struct DrawContext<'a> {
current_node: &'a LayoutNode,
current_offset: (u16, u16),
}
impl<'a> DrawContext<'a> {
/// Creates a new [`DrawContext`], with the offset set to `(0, 0)`.
pub(crate) fn root(root: &'a LayoutNode) -> DrawContext<'a> {
DrawContext {
current_node: root,
current_offset: (0, 0),
}
}
pub(crate) fn rect(&self) -> Rect {
let mut rect = self.current_node.rect;
rect.x += self.current_offset.0;
rect.y += self.current_offset.1;
rect
}
pub(crate) fn should_draw(&self) -> bool {
self.current_node.rect.area() != 0
}
pub(crate) fn children(&'a self) -> impl Iterator<Item = DrawContext<'_>> {
let new_offset = (
self.current_offset.0 + self.current_node.rect.x,
self.current_offset.1 + self.current_node.rect.y,
);
self.current_node
.children
.iter()
.map(move |layout_node| DrawContext {
current_node: layout_node,
current_offset: new_offset,
})
}
}

11
src/tuine/context/mod.rs Normal file
View File

@ -0,0 +1,11 @@
pub mod state_map;
pub use state_map::StateMap;
pub mod draw_context;
pub use draw_context::DrawContext;
pub mod view_context;
pub use view_context::ViewContext;
pub mod state_context;
pub use state_context::StateContext;

View File

@ -0,0 +1,19 @@
use crate::tuine::{Key, State, StateMap};
pub struct StateContext<'a> {
state_map: &'a mut StateMap,
}
impl<'a> StateContext<'a> {
pub fn new(state_map: &'a mut StateMap) -> Self {
Self { state_map }
}
pub fn state<S: State + Default + 'static>(&mut self, key: Key) -> &S {
self.state_map.state::<S>(key)
}
pub fn mut_state<S: State + Default + 'static>(&mut self, key: Key) -> &mut S {
self.state_map.mut_state::<S>(key)
}
}

View File

@ -0,0 +1,38 @@
use rustc_hash::FxHashMap;
use crate::tuine::{Key, State};
#[derive(Default)]
pub struct StateMap(FxHashMap<Key, (Box<dyn State>, bool)>);
impl StateMap {
pub fn state<S: State + Default + 'static>(&mut self, key: Key) -> &S {
let state = self
.0
.entry(key)
.or_insert_with(|| (Box::new(S::default()), true));
state.1 = true;
state
.0
.as_any()
.downcast_ref()
.expect("Successful downcast of state.")
}
pub fn mut_state<S: State + Default + 'static>(&mut self, key: Key) -> &mut S {
let state = self
.0
.entry(key)
.or_insert_with(|| (Box::new(S::default()), true));
state.1 = true;
state
.0
.as_mut_any()
.downcast_mut()
.expect("Successful downcast of state.")
}
}

View File

@ -0,0 +1,30 @@
use crate::tuine::{Caller, Key, State, StateMap};
use super::StateContext;
pub struct ViewContext<'a> {
key_counter: usize,
state_context: StateContext<'a>,
}
impl<'a> ViewContext<'a> {
pub fn new(state_map: &'a mut StateMap) -> Self {
Self {
key_counter: 0,
state_context: StateContext::new(state_map),
}
}
pub fn register_component<C: Into<Caller>>(&mut self, caller: C) -> Key {
self.key_counter += 1;
Key::new(caller.into(), self.key_counter)
}
pub fn state<S: State + Default + 'static>(&mut self, key: Key) -> &S {
self.state_context.state(key)
}
pub fn mut_state<S: State + Default + 'static>(&mut self, key: Key) -> &mut S {
self.state_context.mut_state(key)
}
}

View File

@ -1,9 +1,9 @@
use enum_dispatch::enum_dispatch;
use tui::{layout::Rect, Frame};
use tui::Frame;
use super::{
Block, Bounds, Carousel, Container, DrawContext, Event, Flex, LayoutNode, Shortcut, Size,
Status, TextTable, TmpComponent,
StateContext, Status, TextTable, TmpComponent,
};
/// An [`Element`] is an instantiated [`Component`].

View File

@ -1,12 +1,12 @@
use std::sync::mpsc::Receiver;
use rustc_hash::FxHashMap;
use tui::{backend::Backend, layout::Rect, Terminal};
use tui::{backend::Backend, Terminal};
use crate::tuine::Status;
use super::{
build_layout_tree, Application, Element, Event, Key, State, StateMap, TmpComponent, ViewContext,
build_layout_tree, Application, DrawContext, Element, Event, LayoutNode, StateContext,
StateMap, TmpComponent, ViewContext,
};
#[derive(Clone, Copy, Debug)]
@ -29,10 +29,11 @@ where
B: Backend,
{
let mut app_data = AppData::default();
let mut layout: LayoutNode = LayoutNode::default();
let mut user_interface = {
let mut ctx = ViewContext::new(&mut app_data.state_map);
let mut ui = application.view(&mut ctx);
draw(&mut ui, terminal)?;
let mut ui = new_user_interface(&mut application, &mut app_data);
draw(&mut ui, terminal, &mut app_data, &mut layout)?;
ui
};
@ -40,24 +41,15 @@ where
if let Ok(event) = receiver.recv() {
match event {
RuntimeEvent::UserInterface(event) => {
let mut messages = vec![];
let rect = Rect::default(); // FIXME: TEMP
match user_interface.on_event(rect, event, &mut messages) {
Status::Captured => {}
Status::Ignored => {
application.global_event_handler(event, &mut messages);
}
}
for msg in messages {
debug!("Message: {:?}", msg); // FIXME: Remove this debug line!
application.update(msg);
}
let mut ctx = ViewContext::new(&mut app_data.state_map);
user_interface = application.view(&mut ctx);
draw(&mut user_interface, terminal)?;
on_event(
&mut application,
&mut user_interface,
&mut app_data,
&mut layout,
event,
);
user_interface = new_user_interface(&mut application, &mut app_data);
draw(&mut user_interface, terminal, &mut app_data, &mut layout)?;
}
RuntimeEvent::Custom(message) => {
application.update(message);
@ -66,10 +58,9 @@ where
width: _,
height: _,
} => {
let mut ctx = ViewContext::new(&mut app_data.state_map);
user_interface = application.view(&mut ctx);
user_interface = new_user_interface(&mut application, &mut app_data);
// FIXME: Also nuke any cache and the like...
draw(&mut user_interface, terminal)?;
draw(&mut user_interface, terminal, &mut app_data, &mut layout)?;
}
}
} else {
@ -77,21 +68,60 @@ where
}
}
application.destroy();
application.destructor();
Ok(())
}
fn draw<M, B>(user_interface: &mut Element<'_, M>, terminal: &mut Terminal<B>) -> anyhow::Result<()>
fn on_event<A>(
application: &mut A, user_interface: &mut Element<'_, A::Message>, app_data: &mut AppData,
layout: &mut LayoutNode, event: Event,
) where
A: Application + 'static,
{
let mut messages = vec![];
let mut state_ctx = StateContext::new(&mut app_data.state_map);
let draw_ctx = DrawContext::root(&layout);
match user_interface.on_event(&mut state_ctx, draw_ctx, event, &mut messages) {
Status::Captured => {
// TODO: What to do on capture?
}
Status::Ignored => {
application.global_event_handler(event, &mut messages);
}
}
for msg in messages {
debug!("Message: {:?}", msg); // FIXME: Remove this debug line!
application.update(msg);
}
}
fn new_user_interface<A>(
application: &mut A, app_data: &mut AppData,
) -> Element<'static, A::Message>
where
A: Application + 'static,
{
let mut ctx = ViewContext::new(&mut app_data.state_map);
application.view(&mut ctx)
}
fn draw<M, B>(
user_interface: &mut Element<'_, M>, terminal: &mut Terminal<B>, app_data: &mut AppData,
layout: &mut LayoutNode,
) -> anyhow::Result<()>
where
B: Backend,
{
terminal.draw(|frame| {
let rect = frame.size();
let layout = build_layout_tree(rect, &user_interface);
let context = super::DrawContext::root(&layout);
*layout = build_layout_tree(rect, &user_interface);
let mut state_ctx = StateContext::new(&mut app_data.state_map);
let draw_ctx = DrawContext::root(&layout);
user_interface.draw(context, frame);
user_interface.draw(&mut state_ctx, draw_ctx, frame);
})?;
Ok(())