Start restructuring chat as widget

This commit is contained in:
Joscha 2022-07-14 15:18:19 +02:00
parent 2ee64c11be
commit 26e988114c
4 changed files with 231 additions and 149 deletions

View file

@ -20,8 +20,9 @@ use toss::terminal::Terminal;
use crate::logger::{LogMsg, Logger}; use crate::logger::{LogMsg, Logger};
use crate::vault::Vault; use crate::vault::Vault;
use self::chat::Chat; use self::chat::{Chat, ChatState};
use self::rooms::Rooms; use self::rooms::Rooms;
use self::widgets::Widget;
#[derive(Debug)] #[derive(Debug)]
pub enum UiEvent { pub enum UiEvent {
@ -45,7 +46,7 @@ pub struct Ui {
mode: Mode, mode: Mode,
rooms: Rooms, rooms: Rooms,
log_chat: Chat<LogMsg, Logger>, log_chat: ChatState<LogMsg, Logger>,
} }
impl Ui { impl Ui {
@ -80,7 +81,7 @@ impl Ui {
event_tx: event_tx.clone(), event_tx: event_tx.clone(),
mode: Mode::Main, mode: Mode::Main,
rooms: Rooms::new(vault, event_tx.clone()), rooms: Rooms::new(vault, event_tx.clone()),
log_chat: Chat::new(logger), log_chat: ChatState::new(logger),
}; };
tokio::select! { tokio::select! {
e = ui.run_main(terminal, event_rx, crossterm_lock) => Ok(e), e = ui.run_main(terminal, event_rx, crossterm_lock) => Ok(e),
@ -166,11 +167,7 @@ impl Ui {
async fn render(&mut self, frame: &mut Frame) -> anyhow::Result<()> { async fn render(&mut self, frame: &mut Frame) -> anyhow::Result<()> {
match self.mode { match self.mode {
Mode::Main => self.rooms.render(frame).await, Mode::Main => self.rooms.render(frame).await,
Mode::Log => { Mode::Log => Box::new(self.log_chat.widget()).render(frame).await,
self.log_chat
.render(frame, Pos::new(0, 0), frame.size())
.await
}
} }
Ok(()) Ok(())
} }
@ -202,7 +199,10 @@ impl Ui {
.handle_key_event(terminal, size, crossterm_lock, event) .handle_key_event(terminal, size, crossterm_lock, event)
.await .await
} }
Mode::Log => self.log_chat.handle_navigation(terminal, size, event).await, Mode::Log => {
// TODO Uncomment
// self.log_chat.handle_navigation(terminal, size, event).await
}
} }
EventHandleResult::Continue EventHandleResult::Continue

View file

@ -1,15 +1,17 @@
mod tree; mod tree;
use std::sync::Arc; use async_trait::async_trait;
use toss::frame::{Frame, Size};
use crossterm::event::KeyEvent;
use parking_lot::FairMutex;
use toss::frame::{Frame, Pos, Size};
use toss::terminal::Terminal;
use crate::store::{Msg, MsgStore}; use crate::store::{Msg, MsgStore};
use self::tree::TreeView; use self::tree::{TreeView, TreeViewState};
use super::widgets::Widget;
///////////
// State //
///////////
pub enum Mode { pub enum Mode {
Tree, Tree,
@ -34,74 +36,108 @@ impl<I> Cursor<I> {
} }
} }
pub struct Chat<M: Msg, S: MsgStore<M>> { pub struct ChatState<M: Msg, S: MsgStore<M>> {
store: S, store: S,
cursor: Option<Cursor<M::Id>>,
mode: Mode, mode: Mode,
tree: TreeView<M>, tree: TreeViewState<M, S>,
// thread: ThreadView, // thread: ThreadView,
// flat: FlatView, // flat: FlatView,
} }
impl<M: Msg, S: MsgStore<M>> Chat<M, S> { impl<M: Msg, S: MsgStore<M> + Clone> ChatState<M, S> {
pub fn new(store: S) -> Self { pub fn new(store: S) -> Self {
Self { Self {
store,
cursor: None,
mode: Mode::Tree, mode: Mode::Tree,
tree: TreeView::new(), tree: TreeViewState::new(store.clone()),
store,
}
} }
} }
impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
pub fn store(&self) -> &S { pub fn store(&self) -> &S {
&self.store &self.store
} }
}
impl<M: Msg, S: MsgStore<M>> Chat<M, S> { pub fn widget(&self) -> Chat<M, S> {
pub async fn handle_navigation(
&mut self,
terminal: &mut Terminal,
size: Size,
event: KeyEvent,
) {
match self.mode { match self.mode {
Mode::Tree => { Mode::Tree => Chat::Tree(self.tree.widget()),
self.tree
.handle_navigation(&mut self.store, &mut self.cursor, terminal, size, event)
.await
} }
} }
} }
pub async fn handle_messaging( impl<M: Msg, S: MsgStore<M>> Chat<M, S> {
&mut self, // pub async fn handle_navigation(
terminal: &mut Terminal, // &mut self,
crossterm_lock: &Arc<FairMutex<()>>, // terminal: &mut Terminal,
event: KeyEvent, // size: Size,
) -> Option<(Option<M::Id>, String)> { // event: KeyEvent,
match self.mode { // ) {
Mode::Tree => { // match self.mode {
self.tree // Mode::Tree => {
.handle_messaging( // self.tree
&mut self.store, // .handle_navigation(&mut self.store, &mut self.cursor, terminal, size, event)
&mut self.cursor, // .await
terminal, // }
crossterm_lock, // }
event, // }
)
.await // pub async fn handle_messaging(
// &mut self,
// terminal: &mut Terminal,
// crossterm_lock: &Arc<FairMutex<()>>,
// event: KeyEvent,
// ) -> Option<(Option<M::Id>, String)> {
// match self.mode {
// Mode::Tree => {
// self.tree
// .handle_messaging(
// &mut self.store,
// &mut self.cursor,
// terminal,
// crossterm_lock,
// event,
// )
// .await
// }
// }
// }
// pub async fn render(&mut self, frame: &mut Frame, pos: Pos, size: Size) {
// match self.mode {
// Mode::Tree => {
// self.tree
// .render(&mut self.store, &self.cursor, frame, pos, size)
// .await
// }
// }
// }
} }
////////////
// Widget //
////////////
pub enum Chat<M: Msg, S: MsgStore<M>> {
Tree(TreeView<M, S>),
}
#[async_trait]
impl<M, S> Widget for Chat<M, S>
where
M: Msg,
M::Id: Send,
S: MsgStore<M> + Send + Sync,
{
fn size(&self, frame: &mut Frame, max_width: Option<u16>, max_height: Option<u16>) -> Size {
match self {
Self::Tree(tree) => tree.size(frame, max_width, max_height),
} }
} }
pub async fn render(&mut self, frame: &mut Frame, pos: Pos, size: Size) { async fn render(self: Box<Self>, frame: &mut Frame) {
match self.mode { match *self {
Mode::Tree => { Self::Tree(tree) => Box::new(tree).render(frame).await,
self.tree
.render(&mut self.store, &self.cursor, frame, pos, size)
.await
}
} }
} }
} }

View file

@ -1,84 +1,126 @@
mod action; // mod action;
mod blocks; // mod blocks;
mod cursor; // mod cursor;
mod layout; // mod layout;
mod render; // mod render;
mod util; // mod util;
use std::marker::PhantomData;
use std::sync::Arc; use std::sync::Arc;
use crossterm::event::{KeyCode, KeyEvent}; use async_trait::async_trait;
use parking_lot::FairMutex; use tokio::sync::Mutex;
use toss::frame::{Frame, Pos, Size}; use toss::frame::{Frame, Size};
use toss::terminal::Terminal;
use crate::store::{Msg, MsgStore}; use crate::store::{Msg, MsgStore};
use crate::ui::widgets::Widget;
use super::Cursor; ///////////
// State //
///////////
pub struct TreeView<M: Msg> { /// The anchor specifies a specific line in a room's history.
// pub focus: Option<M::Id>, enum Anchor<I> {
// pub folded: HashSet<M::Id>, /// The bottom of the room's history stays fixed.
// pub minimized: HashSet<M::Id>, Bottom,
phantom: PhantomData<M::Id>, // TODO Remove /// The top of a message stays fixed.
Msg(I),
/// The line after a message's subtree stays fixed.
After(I),
} }
impl<M: Msg> TreeView<M> { struct Compose<I> {
pub fn new() -> Self { /// The message that the cursor was on when composing began, or `None` if it
/// was [`Cursor::Bottom`].
///
/// Used to jump back to the original position when composing is aborted
/// because the editor may be moved during composing.
coming_from: Option<I>,
/// The parent message of this reply, or `None` if it will be a new
/// top-level message.
parent: Option<I>,
// TODO Editor state
// TODO Whether currently editing or moving cursor
}
struct Placeholder<I> {
/// See [`Composing::coming_from`].
coming_from: Option<I>,
/// See [`Composing::parent`].
after: Option<I>,
}
enum Cursor<I> {
/// No cursor visible because it is at the bottom of the chat history.
///
/// See also [`Anchor::Bottom`].
Bottom,
/// The cursor points to a message.
///
/// See also [`Anchor::Msg`].
Msg(I),
/// The cursor has turned into an editor because we're composing a new
/// message.
///
/// See also [`Anchor::After`].
Compose(Compose<I>),
/// A placeholder message is being displayed for a message that was just
/// sent by the user.
///
/// Will be replaced by a [`Cursor::Msg`] as soon as the server replies to
/// the send command with the sent message. Otherwise, it will
///
/// See also [`Anchor::After`].
Placeholder(Placeholder<I>),
}
struct InnerTreeViewState<M: Msg, S: MsgStore<M>> {
store: S,
anchor: Anchor<M::Id>,
anchor_line: i32,
cursor: Cursor<M::Id>,
}
impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
fn new(store: S) -> Self {
Self { Self {
phantom: PhantomData, store,
anchor: Anchor::Bottom,
anchor_line: 0,
cursor: Cursor::Bottom,
}
} }
} }
pub async fn handle_navigation<S: MsgStore<M>>( pub struct TreeViewState<M: Msg, S: MsgStore<M>>(Arc<Mutex<InnerTreeViewState<M, S>>>);
&mut self,
s: &mut S, impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
c: &mut Option<Cursor<M::Id>>, pub fn new(store: S) -> Self {
t: &mut Terminal, Self(Arc::new(Mutex::new(InnerTreeViewState::new(store))))
z: Size, }
event: KeyEvent,
) { pub fn widget(&self) -> TreeView<M, S> {
match event.code { TreeView(self.0.clone())
KeyCode::Char('k') => self.move_up(s, c, t.frame(), z).await,
KeyCode::Char('j') => self.move_down(s, c, t.frame(), z).await,
KeyCode::Char('K') => self.move_up_sibling(s, c, t.frame(), z).await,
KeyCode::Char('J') => self.move_down_sibling(s, c, t.frame(), z).await,
KeyCode::Char('z') | KeyCode::Char('Z') => self.center_cursor(s, c, t.frame(), z).await,
KeyCode::Char('g') => self.move_to_first(s, c, t.frame(), z).await,
KeyCode::Char('G') => self.move_to_last(s, c, t.frame(), z).await,
KeyCode::Esc => *c = None, // TODO Make 'G' do the same thing?
_ => {}
} }
} }
pub async fn handle_messaging<S: MsgStore<M>>( ////////////
&mut self, // Widget //
s: &mut S, ////////////
c: &mut Option<Cursor<M::Id>>,
t: &mut Terminal, pub struct TreeView<M: Msg, S: MsgStore<M>>(Arc<Mutex<InnerTreeViewState<M, S>>>);
l: &Arc<FairMutex<()>>,
event: KeyEvent, #[async_trait]
) -> Option<(Option<M::Id>, String)> { impl<M, S> Widget for TreeView<M, S>
match event.code { where
KeyCode::Char('r') => Self::reply_normal(s, c, t, l).await, M: Msg,
KeyCode::Char('R') => Self::reply_alternate(s, c, t, l).await, M::Id: Send,
KeyCode::Char('t') | KeyCode::Char('T') => Self::create_new_thread(t, l).await, S: MsgStore<M> + Send + Sync,
_ => None, {
} fn size(&self, _frame: &mut Frame, _max_width: Option<u16>, _max_height: Option<u16>) -> Size {
Size::ZERO
} }
pub async fn render<S: MsgStore<M>>( async fn render(self: Box<Self>, frame: &mut Frame) {
&mut self, todo!()
store: &mut S,
cursor: &Option<Cursor<M::Id>>,
frame: &mut Frame,
pos: Pos,
size: Size,
) {
let blocks = self
.layout_blocks(store, cursor.as_ref(), frame, size)
.await;
Self::render_blocks(frame, pos, size, blocks);
} }
} }

View file

@ -13,7 +13,7 @@ use crate::euph::api::{SessionType, SessionView};
use crate::euph::{self, Joined, Status}; use crate::euph::{self, Joined, Status};
use crate::vault::{EuphMsg, EuphVault}; use crate::vault::{EuphMsg, EuphVault};
use super::chat::Chat; use super::chat::ChatState;
use super::widgets::background::Background; use super::widgets::background::Background;
use super::widgets::empty::Empty; use super::widgets::empty::Empty;
use super::widgets::list::{List, ListState}; use super::widgets::list::{List, ListState};
@ -24,7 +24,7 @@ use super::{util, UiEvent};
pub struct EuphRoom { pub struct EuphRoom {
ui_event_tx: mpsc::UnboundedSender<UiEvent>, ui_event_tx: mpsc::UnboundedSender<UiEvent>,
room: Option<euph::Room>, room: Option<euph::Room>,
chat: Chat<EuphMsg, EuphVault>, chat: ChatState<EuphMsg, EuphVault>,
nick_list_width: u16, nick_list_width: u16,
nick_list: ListState<String>, nick_list: ListState<String>,
@ -35,7 +35,7 @@ impl EuphRoom {
Self { Self {
ui_event_tx, ui_event_tx,
room: None, room: None,
chat: Chat::new(vault), chat: ChatState::new(vault),
nick_list_width: 24, nick_list_width: 24,
nick_list: ListState::new(), nick_list: ListState::new(),
} }
@ -100,7 +100,9 @@ impl EuphRoom {
let chat_pos = Pos::new(0, hsplit + 1); let chat_pos = Pos::new(0, hsplit + 1);
let chat_size = Size::new(size.width, size.height.saturating_sub(hsplit as u16 + 1)); let chat_size = Size::new(size.width, size.height.saturating_sub(hsplit as u16 + 1));
self.chat.render(frame, chat_pos, chat_size).await; frame.push(chat_pos, chat_size);
Box::new(self.chat.widget()).render(frame).await;
frame.pop();
self.render_status(frame, status_pos, status); self.render_status(frame, status_pos, status);
Self::render_hsplit(frame, hsplit); Self::render_hsplit(frame, hsplit);
} }
@ -127,7 +129,9 @@ impl EuphRoom {
let nick_list_pos = Pos::new(vsplit + 1, 0); let nick_list_pos = Pos::new(vsplit + 1, 0);
let nick_list_size = Size::new(self.nick_list_width, size.height); let nick_list_size = Size::new(self.nick_list_width, size.height);
self.chat.render(frame, chat_pos, chat_size).await; frame.push(chat_pos, chat_size);
Box::new(self.chat.widget()).render(frame).await;
frame.pop();
self.render_status(frame, status_pos, status); self.render_status(frame, status_pos, status);
self.render_nick_list(frame, nick_list_pos, nick_list_size, joined) self.render_nick_list(frame, nick_list_pos, nick_list_size, joined)
.await; .await;
@ -297,9 +301,9 @@ impl EuphRoom {
let hsplit = 1_i32; let hsplit = 1_i32;
let chat_size = Size::new(vsplit as u16, size.height.saturating_sub(hsplit as u16 + 1)); let chat_size = Size::new(vsplit as u16, size.height.saturating_sub(hsplit as u16 + 1));
self.chat // self.chat
.handle_navigation(terminal, chat_size, event) // .handle_navigation(terminal, chat_size, event)
.await; // .await;
if let Some(room) = &self.room { if let Some(room) = &self.room {
if let Ok(Some(Status::Joined(_))) = room.status().await { if let Ok(Some(Status::Joined(_))) = room.status().await {
@ -309,13 +313,13 @@ impl EuphRoom {
} }
} }
if let Some((parent, content)) = self // if let Some((parent, content)) = self
.chat // .chat
.handle_messaging(terminal, crossterm_lock, event) // .handle_messaging(terminal, crossterm_lock, event)
.await // .await
{ // {
let _ = room.send(parent, content); // let _ = room.send(parent, content);
} // }
} }
} }
} }