diff --git a/CHANGELOG.md b/CHANGELOG.md index f9b8690..590f976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ Procedure when bumping the version number: ## Unreleased +### Changed +- Reduced amount of unnecessary redraws + ### Fixed - Crash when connecting to nonexistent rooms - Crash when connecting to rooms that require authentication diff --git a/src/euph.rs b/src/euph.rs index 2f4f2f2..ab93753 100644 --- a/src/euph.rs +++ b/src/euph.rs @@ -2,6 +2,6 @@ mod room; mod small_message; mod util; -pub use room::Room; -pub use small_message::SmallMessage; -pub use util::{nick_color, nick_style}; +pub use room::*; +pub use small_message::*; +pub use util::*; diff --git a/src/euph/room.rs b/src/euph/room.rs index 3cb44e6..558ba2c 100644 --- a/src/euph/room.rs +++ b/src/euph/room.rs @@ -17,7 +17,6 @@ use tokio_tungstenite::tungstenite::handshake::client::Response; use tokio_tungstenite::tungstenite::http::{header, HeaderValue}; use crate::macros::ok_or_return; -use crate::ui::UiEvent; use crate::vault::{EuphVault, Vault}; #[derive(Debug, thiserror::Error)] @@ -26,11 +25,19 @@ pub enum Error { Stopped, } +pub enum EuphRoomEvent { + Connected, + Disconnected, + Data(Box), +} + #[derive(Debug)] enum Event { + // Events Connected(ConnTx), Disconnected, Data(Box), + // Commands Status(oneshot::Sender>), RequestLogs, Nick(String), @@ -41,7 +48,6 @@ enum Event { struct State { name: String, vault: EuphVault, - ui_event_tx: mpsc::UnboundedSender, conn_tx: Option, /// `None` before any `snapshot-event`, then either `Some(None)` or @@ -56,6 +62,7 @@ impl State { canary: oneshot::Receiver, event_tx: mpsc::UnboundedSender, mut event_rx: mpsc::UnboundedReceiver, + euph_room_event_tx: mpsc::UnboundedSender, ) { let vault = self.vault.clone(); let name = self.name.clone(); @@ -63,7 +70,7 @@ impl State { _ = canary => Ok(()), _ = Self::reconnect(&vault, &name, &event_tx) => Ok(()), _ = Self::regularly_request_logs(&event_tx) => Ok(()), - e = self.handle_events(&mut event_rx) => e, + e = self.handle_events(&mut event_rx, &euph_room_event_tx) => e, }; if let Err(e) = result { @@ -150,22 +157,23 @@ impl State { async fn handle_events( &mut self, event_rx: &mut mpsc::UnboundedReceiver, + euph_room_event_tx: &mpsc::UnboundedSender, ) -> anyhow::Result<()> { while let Some(event) = event_rx.recv().await { - // TODO Send UI events on more occasions - // Example: When a room disconnects at the moment, the screen is - // redrawn. Why? Because tungstenite debug-logs that the connection - // was closed and the log then causes a full-screen redraw. This is - // a clear case of "works but only because mistakes cancel out". A - // first step towards fixing this would be to only redraw if an - // event affected the currently visible screen. match event { - Event::Connected(conn_tx) => self.conn_tx = Some(conn_tx), + Event::Connected(conn_tx) => { + self.conn_tx = Some(conn_tx); + let _ = euph_room_event_tx.send(EuphRoomEvent::Connected); + } Event::Disconnected => { self.conn_tx = None; self.last_msg_id = None; + let _ = euph_room_event_tx.send(EuphRoomEvent::Disconnected); + } + Event::Data(data) => { + self.on_data(&*data).await?; + let _ = euph_room_event_tx.send(EuphRoomEvent::Data(data)); } - Event::Data(data) => self.on_data(*data).await?, Event::Status(reply_tx) => self.on_status(reply_tx).await, Event::RequestLogs => self.on_request_logs(), Event::Nick(name) => self.on_nick(name), @@ -182,7 +190,7 @@ impl State { }) } - async fn on_data(&mut self, data: Data) -> anyhow::Result<()> { + async fn on_data(&mut self, data: &Data) -> anyhow::Result<()> { match data { Data::BounceEvent(_) => {} Data::DisconnectEvent(d) => { @@ -217,7 +225,8 @@ impl State { let own_user_id = self.own_user_id().await; if let Some(last_msg_id) = &mut self.last_msg_id { let id = d.0.id; - self.vault.add_message(d.0, *last_msg_id, own_user_id); + self.vault + .add_message(d.0.clone(), *last_msg_id, own_user_id); *last_msg_id = Some(id); } else { bail!("send event before snapshot event"); @@ -228,17 +237,19 @@ impl State { self.vault.join(Time::now()); self.last_msg_id = Some(d.log.last().map(|m| m.id)); let own_user_id = self.own_user_id().await; - self.vault.add_messages(d.log, None, own_user_id); + self.vault.add_messages(d.log.clone(), None, own_user_id); } Data::LogReply(d) => { let own_user_id = self.own_user_id().await; - self.vault.add_messages(d.log, d.before, own_user_id); + self.vault + .add_messages(d.log.clone(), d.before, own_user_id); } Data::SendReply(d) => { let own_user_id = self.own_user_id().await; if let Some(last_msg_id) = &mut self.last_msg_id { let id = d.0.id; - self.vault.add_message(d.0, *last_msg_id, own_user_id); + self.vault + .add_message(d.0.clone(), *last_msg_id, own_user_id); *last_msg_id = Some(id); } else { bail!("send reply before snapshot event"); @@ -246,7 +257,6 @@ impl State { } _ => {} } - let _ = self.ui_event_tx.send(UiEvent::Redraw); Ok(()) } @@ -332,25 +342,26 @@ pub struct Room { } impl Room { - pub fn new(vault: EuphVault, ui_event_tx: mpsc::UnboundedSender) -> Self { + pub fn new(vault: EuphVault) -> (Self, mpsc::UnboundedReceiver) { let (canary_tx, canary_rx) = oneshot::channel(); let (event_tx, event_rx) = mpsc::unbounded_channel(); + let (euph_room_event_tx, euph_room_event_rx) = mpsc::unbounded_channel(); let state = State { name: vault.room().to_string(), vault, - ui_event_tx, conn_tx: None, last_msg_id: None, requesting_logs: Arc::new(Mutex::new(false)), }; - task::spawn(state.run(canary_rx, event_tx.clone(), event_rx)); + task::spawn(state.run(canary_rx, event_tx.clone(), event_rx, euph_room_event_tx)); - Self { + let new_room = Self { canary: canary_tx, event_tx, - } + }; + (new_room, euph_room_event_rx) } pub fn stopped(&self) -> bool { diff --git a/src/ui.rs b/src/ui.rs index 4b31d89..8de77a5 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -6,17 +6,20 @@ mod util; mod widgets; use std::convert::Infallible; +use std::io; use std::sync::{Arc, Weak}; use std::time::{Duration, Instant}; -use crossterm::event::{Event, KeyCode}; +use crossterm::event::KeyCode; use parking_lot::FairMutex; use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; use tokio::task; use toss::terminal::Terminal; +use crate::euph::EuphRoomEvent; use crate::logger::{LogMsg, Logger}; +use crate::macros::{ok_or_return, some_or_return}; use crate::vault::Vault; pub use self::chat::ChatMsg; @@ -30,17 +33,20 @@ use self::widgets::BoxedWidget; /// Time to spend batch processing events before redrawing the screen. const EVENT_PROCESSING_TIME: Duration = Duration::from_millis(1000 / 15); // 15 fps -#[derive(Debug)] pub enum UiEvent { - Redraw, - Term(Event), + GraphemeWidthsChanged, + LogChanged, + Term(crossterm::event::Event), + EuphRoom { name: String, event: EuphRoomEvent }, } enum EventHandleResult { + Redraw, Continue, Stop, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Mode { Main, Log, @@ -92,34 +98,34 @@ impl Ui { key_bindings_list: None, }; tokio::select! { - e = ui.run_main(terminal, event_rx, crossterm_lock) => Ok(e), - _ = Self::update_on_log_event(logger_rx, &event_tx) => Ok(Ok(())), - e = crossterm_event_task => e, - }? + e = ui.run_main(terminal, event_rx, crossterm_lock) => e?, + _ = Self::update_on_log_event(logger_rx, &event_tx) => (), + e = crossterm_event_task => e??, + } + Ok(()) } fn poll_crossterm_events( tx: UnboundedSender, lock: Weak>, - ) -> anyhow::Result<()> { - while let Some(lock) = lock.upgrade() { + ) -> crossterm::Result<()> { + loop { + let lock = some_or_return!(lock.upgrade(), Ok(())); let _guard = lock.lock(); if crossterm::event::poll(Self::POLL_DURATION)? { let event = crossterm::event::read()?; - tx.send(UiEvent::Term(event))?; + ok_or_return!(tx.send(UiEvent::Term(event)), Ok(())); } } - Ok(()) } async fn update_on_log_event( mut logger_rx: UnboundedReceiver<()>, event_tx: &UnboundedSender, ) { - while let Some(()) = logger_rx.recv().await { - if event_tx.send(UiEvent::Redraw).is_err() { - break; - } + loop { + some_or_return!(logger_rx.recv().await); + ok_or_return!(event_tx.send(UiEvent::LogChanged)); } } @@ -128,7 +134,7 @@ impl Ui { terminal: &mut Terminal, mut event_rx: UnboundedReceiver, crossterm_lock: Arc>, - ) -> anyhow::Result<()> { + ) -> io::Result<()> { // Initial render so we don't show a blank screen until the first event terminal.autoresize()?; terminal.frame().reset(); @@ -140,7 +146,7 @@ impl Ui { if terminal.measuring_required() { let _guard = crossterm_lock.lock(); terminal.measure_widths()?; - self.event_tx.send(UiEvent::Redraw)?; + ok_or_return!(self.event_tx.send(UiEvent::GraphemeWidthsChanged), Ok(())); } // 2. Handle events (in batches) @@ -148,25 +154,11 @@ impl Ui { Some(event) => event, None => return Ok(()), }; + let mut redraw = false; let end_time = Instant::now() + EVENT_PROCESSING_TIME; loop { - // Render in-between events so the next event is handled in an - // up-to-date state. The results of these intermediate renders - // will be thrown away before the final render. - terminal.autoresize()?; - self.widget().await.render(terminal.frame()).await; - - let result = match event { - UiEvent::Redraw => EventHandleResult::Continue, - UiEvent::Term(event) => { - if let Some(event) = InputEvent::from_event(event) { - self.handle_event(terminal, &crossterm_lock, &event).await - } else { - EventHandleResult::Continue - } - } - }; - match result { + match self.handle_event(terminal, &crossterm_lock, event).await { + EventHandleResult::Redraw => redraw = true, EventHandleResult::Continue => {} EventHandleResult::Stop => return Ok(()), } @@ -181,10 +173,12 @@ impl Ui { } // 3. Render and present final state - terminal.autoresize()?; - terminal.frame().reset(); - self.widget().await.render(terminal.frame()).await; - terminal.present()?; + if redraw { + terminal.autoresize()?; + terminal.frame().reset(); + self.widget().await.render(terminal.frame()).await; + terminal.present()?; + } } } @@ -224,8 +218,35 @@ impl Ui { &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, - event: &InputEvent, + event: UiEvent, ) -> EventHandleResult { + match event { + UiEvent::GraphemeWidthsChanged => EventHandleResult::Redraw, + UiEvent::LogChanged if self.mode == Mode::Log => EventHandleResult::Redraw, + UiEvent::LogChanged => EventHandleResult::Continue, + UiEvent::Term(event) => { + self.handle_term_event(terminal, crossterm_lock, event) + .await + } + UiEvent::EuphRoom { name, event } => { + let handled = self.handle_euph_room_event(name, event).await; + if self.mode == Mode::Main && handled { + EventHandleResult::Redraw + } else { + EventHandleResult::Continue + } + } + } + } + + async fn handle_term_event( + &mut self, + terminal: &mut Terminal, + crossterm_lock: &Arc>, + event: crossterm::event::Event, + ) -> EventHandleResult { + let event = some_or_return!(InputEvent::from_event(event), EventHandleResult::Continue); + if let key!(Ctrl + 'c') = event { // Exit unconditionally on ctrl+c. Previously, shift+q would also // unconditionally exit, but that interfered with typing text in @@ -239,35 +260,35 @@ impl Ui { key!(Esc) | key!(F 1) | key!('?') => self.key_bindings_list = None, key!('k') | key!(Up) => key_bindings_list.scroll_up(1), key!('j') | key!(Down) => key_bindings_list.scroll_down(1), - _ => {} + _ => return EventHandleResult::Continue, } - return EventHandleResult::Continue; + return EventHandleResult::Redraw; } match event { key!(F 1) => { self.key_bindings_list = Some(ListState::new()); - return EventHandleResult::Continue; + return EventHandleResult::Redraw; } key!(F 12) => { self.mode = match self.mode { Mode::Main => Mode::Log, Mode::Log => Mode::Main, }; - return EventHandleResult::Continue; + return EventHandleResult::Redraw; } _ => {} } - let handled = match self.mode { + let mut handled = match self.mode { Mode::Main => { self.rooms - .handle_event(terminal, crossterm_lock, event) + .handle_input_event(terminal, crossterm_lock, &event) .await } Mode::Log => self .log_chat - .handle_event(terminal, crossterm_lock, event, false) + .handle_input_event(terminal, crossterm_lock, &event, false) .await .handled(), }; @@ -278,9 +299,19 @@ impl Ui { if !handled { if let key!('?') = event { self.show_key_bindings(); + handled = true; } } - EventHandleResult::Continue + if handled { + EventHandleResult::Redraw + } else { + EventHandleResult::Continue + } + } + + async fn handle_euph_room_event(&mut self, name: String, event: EuphRoomEvent) -> bool { + // TODO Redirect this to the euph room + true } } diff --git a/src/ui/chat.rs b/src/ui/chat.rs index 05a20d5..d4736de 100644 --- a/src/ui/chat.rs +++ b/src/ui/chat.rs @@ -90,7 +90,7 @@ impl> ChatState { } } - pub async fn handle_event( + pub async fn handle_input_event( &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, @@ -100,7 +100,7 @@ impl> ChatState { match self.mode { Mode::Tree => { self.tree - .handle_event(terminal, crossterm_lock, event, can_compose) + .handle_input_event(terminal, crossterm_lock, event, can_compose) .await } } diff --git a/src/ui/chat/tree.rs b/src/ui/chat/tree.rs index b421886..4087094 100644 --- a/src/ui/chat/tree.rs +++ b/src/ui/chat/tree.rs @@ -79,7 +79,7 @@ impl> InnerTreeViewState { bindings.binding("z", "center cursor on screen"); } - async fn handle_movement_event(&mut self, frame: &mut Frame, event: &InputEvent) -> bool { + async fn handle_movement_input_event(&mut self, frame: &mut Frame, event: &InputEvent) -> bool { let chat_height = frame.size().height - 3; match event { @@ -115,7 +115,7 @@ impl> InnerTreeViewState { bindings.binding("ctrl+s", "mark all older messages as seen"); } - async fn handle_action_event(&mut self, event: &InputEvent, id: Option<&M::Id>) -> bool { + async fn handle_action_input_event(&mut self, event: &InputEvent, id: Option<&M::Id>) -> bool { match event { key!(' ') => { if let Some(id) = id { @@ -162,7 +162,7 @@ impl> InnerTreeViewState { bindings.binding("t", "start a new thread"); } - async fn handle_edit_initiating_event( + async fn handle_edit_initiating_input_event( &mut self, event: &InputEvent, id: Option, @@ -198,7 +198,7 @@ impl> InnerTreeViewState { } } - async fn handle_normal_event( + async fn handle_normal_input_event( &mut self, frame: &mut Frame, event: &InputEvent, @@ -206,12 +206,12 @@ impl> InnerTreeViewState { id: Option, ) -> bool { #[allow(clippy::if_same_then_else)] - if self.handle_movement_event(frame, event).await { + if self.handle_movement_input_event(frame, event).await { true - } else if self.handle_action_event(event, id.as_ref()).await { + } else if self.handle_action_input_event(event, id.as_ref()).await { true } else if can_compose { - self.handle_edit_initiating_event(event, id).await + self.handle_edit_initiating_input_event(event, id).await } else { false } @@ -223,7 +223,7 @@ impl> InnerTreeViewState { util::list_editor_key_bindings(bindings, |_| true, true); } - fn handle_editor_event( + fn handle_editor_input_event( &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, @@ -251,7 +251,7 @@ impl> InnerTreeViewState { } _ => { - let handled = util::handle_editor_event( + let handled = util::handle_editor_input_event( &self.editor, terminal, crossterm_lock, @@ -282,7 +282,7 @@ impl> InnerTreeViewState { } } - async fn handle_event( + async fn handle_input_event( &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, @@ -292,7 +292,7 @@ impl> InnerTreeViewState { match &self.cursor { Cursor::Bottom => { if self - .handle_normal_event(terminal.frame(), event, can_compose, None) + .handle_normal_input_event(terminal.frame(), event, can_compose, None) .await { Reaction::Handled @@ -303,7 +303,7 @@ impl> InnerTreeViewState { Cursor::Msg(id) => { let id = id.clone(); if self - .handle_normal_event(terminal.frame(), event, can_compose, Some(id)) + .handle_normal_input_event(terminal.frame(), event, can_compose, Some(id)) .await { Reaction::Handled @@ -314,7 +314,7 @@ impl> InnerTreeViewState { Cursor::Editor { coming_from, parent, - } => self.handle_editor_event( + } => self.handle_editor_input_event( terminal, crossterm_lock, event, @@ -322,7 +322,10 @@ impl> InnerTreeViewState { parent.clone(), ), Cursor::Pseudo { .. } => { - if self.handle_movement_event(terminal.frame(), event).await { + if self + .handle_movement_input_event(terminal.frame(), event) + .await + { Reaction::Handled } else { Reaction::NotHandled @@ -365,7 +368,7 @@ impl> TreeViewState { self.0.lock().await.list_key_bindings(bindings, can_compose); } - pub async fn handle_event( + pub async fn handle_input_event( &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, @@ -375,7 +378,7 @@ impl> TreeViewState { self.0 .lock() .await - .handle_event(terminal, crossterm_lock, event, can_compose) + .handle_input_event(terminal, crossterm_lock, event, can_compose) .await } diff --git a/src/ui/euph/room.rs b/src/ui/euph/room.rs index 85c266c..37a499d 100644 --- a/src/ui/euph/room.rs +++ b/src/ui/euph/room.rs @@ -11,7 +11,8 @@ use tokio::sync::{mpsc, oneshot}; use toss::styled::Styled; use toss::terminal::Terminal; -use crate::euph; +use crate::euph::{self, EuphRoomEvent}; +use crate::macros::{ok_or_return, some_or_return}; use crate::store::MsgStore; use crate::ui::chat::{ChatState, Reaction}; use crate::ui::input::{key, InputEvent, KeyBindingsList, KeyEvent}; @@ -61,10 +62,32 @@ impl EuphRoom { } } + async fn shovel_room_events( + name: String, + mut euph_room_event_rx: mpsc::UnboundedReceiver, + ui_event_tx: mpsc::UnboundedSender, + ) { + loop { + let event = some_or_return!(euph_room_event_rx.recv().await); + let event = UiEvent::EuphRoom { + name: name.clone(), + event, + }; + ok_or_return!(ui_event_tx.send(event)); + } + } + pub fn connect(&mut self) { if self.room.is_none() { - self.room = Some(euph::Room::new( - self.chat.store().clone(), + let store = self.chat.store().clone(); + let name = store.room().to_string(); + let (room, euph_room_event_rx) = euph::Room::new(store); + + self.room = Some(room); + + tokio::task::spawn(Self::shovel_room_events( + name, + euph_room_event_rx, self.ui_event_tx.clone(), )); } @@ -353,7 +376,7 @@ impl EuphRoom { } } - pub async fn handle_event( + pub async fn handle_input_event( &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, @@ -366,7 +389,7 @@ impl EuphRoom { if let Ok(Some(Status::Joined(joined))) = room.status().await { match self .chat - .handle_event(terminal, crossterm_lock, event, true) + .handle_input_event(terminal, crossterm_lock, event, true) .await { Reaction::NotHandled => {} @@ -392,7 +415,7 @@ impl EuphRoom { } self.chat - .handle_event(terminal, crossterm_lock, event, false) + .handle_input_event(terminal, crossterm_lock, event, false) .await .handled() } @@ -408,7 +431,7 @@ impl EuphRoom { self.state = State::Normal; true } - _ => util::handle_editor_event( + _ => util::handle_editor_input_event( ed, terminal, crossterm_lock, diff --git a/src/ui/rooms.rs b/src/ui/rooms.rs index 832c5b3..290142f 100644 --- a/src/ui/rooms.rs +++ b/src/ui/rooms.rs @@ -278,7 +278,7 @@ impl Rooms { } } - pub async fn handle_event( + pub async fn handle_input_event( &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, @@ -326,7 +326,10 @@ impl Rooms { }, State::ShowRoom(name) => { if let Some(room) = self.euph_rooms.get_mut(name) { - if room.handle_event(terminal, crossterm_lock, event).await { + if room + .handle_input_event(terminal, crossterm_lock, event) + .await + { return true; } @@ -348,7 +351,7 @@ impl Rooms { } } _ => { - return util::handle_editor_event( + return util::handle_editor_input_event( ed, terminal, crossterm_lock, diff --git a/src/ui/util.rs b/src/ui/util.rs index 52ae0dc..280854d 100644 --- a/src/ui/util.rs +++ b/src/ui/util.rs @@ -59,7 +59,7 @@ pub fn list_editor_key_bindings( bindings.binding("↑/↓", "move cursor up/down"); } -pub fn handle_editor_event( +pub fn handle_editor_input_event( editor: &EditorState, terminal: &mut Terminal, crossterm_lock: &Arc>,