Skip to content

Commit

Permalink
refactoring in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
alexpasmantier committed Sep 17, 2024
1 parent 4ba038c commit 52c48bb
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 58 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ signal-hook = "0.3.17"
strip-ansi-escapes = "0.2.0"
strum = { version = "0.26.3", features = ["derive"] }
tokio = { version = "1.39.3", features = ["full"] }
tokio-stream = "0.1.16"
tokio-util = "0.7.11"
tracing = "0.1.40"
tracing-error = "0.2.0"
Expand Down
125 changes: 68 additions & 57 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
/// │ Render component │ │ Update component │
/// └──────────────────┘ └──────────────────┘
///
use std::sync::Arc;
use std::sync::{Arc, Mutex};

use color_eyre::Result;
use crossterm::event::KeyEvent;
use ratatui::prelude::Rect;
use serde::{Deserialize, Serialize};
use tokio::sync::{mpsc, Mutex};
use tokio::sync::mpsc;
use tracing::{debug, info};

use crate::{
Expand All @@ -52,6 +52,8 @@ pub struct App {
last_tick_key_events: Vec<KeyEvent>,
action_tx: mpsc::UnboundedSender<Action>,
action_rx: mpsc::UnboundedReceiver<Action>,
render_tx: mpsc::UnboundedSender<RenderingTask>,
render_rx: mpsc::UnboundedReceiver<RenderingTask>,
}

#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
Expand All @@ -63,6 +65,7 @@ pub enum Mode {
impl App {
pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> {
let (action_tx, action_rx) = mpsc::unbounded_channel();
let (render_tx, mut render_rx) = mpsc::unbounded_channel();
Ok(Self {
tick_rate,
frame_rate,
Expand All @@ -77,51 +80,23 @@ impl App {
last_tick_key_events: Vec::new(),
action_tx,
action_rx,
render_tx,
render_rx,
})
}

pub async fn run(&mut self) -> Result<()> {
// Rendering loop
let (render_tx, mut render_rx) = mpsc::unbounded_channel();
let mut tui = Tui::new()?
.tick_rate(self.tick_rate)
.frame_rate(self.frame_rate);
// this starts the event handling loop in Tui
tui.enter();

// Rendering loop
tokio::spawn(async move {
let tui = Tui::new()?
.tick_rate(self.tick_rate)
.frame_rate(self.frame_rate);
tui.enter();

let components = self.components.clone();
for component in self.components.lock().await.iter_mut() {
component.register_action_handler(self.action_tx.clone())?;
}
for component in self.components.lock().await.iter_mut() {
component.register_config_handler(self.config.clone())?;
}
for component in self.components.lock().await.iter_mut() {
component.init(tui.size()?)?;
}

loop {
// add
if let Some(_) = render_rx.recv().await {
let c = components.lock().await;
tui.terminal.draw(|frame| {
for component in c.iter() {
if let Err(err) = component.draw(frame, frame.area()) {
let _ = self
.action_tx
.send(Action::Error(format!("Failed to draw: {:?}", err)));
}
}
})?;
}
}

tui.exit()
self.render(tui).await;
});

// Event handling loop

// Action handling loop
let action_tx = self.action_tx.clone();
loop {
Expand All @@ -141,6 +116,8 @@ impl App {
Ok(())
}

/// Handle incoming events and produce actions if necessary. These actions are then sent to the
/// action channel for processing.
async fn handle_events(&mut self, tui: &mut Tui) -> Result<()> {
let Some(event) = tui.next_event().await else {
return Ok(());
Expand All @@ -154,7 +131,7 @@ impl App {
Event::Key(key) => self.handle_key_event(key)?,
_ => {}
}
for component in self.components.iter_mut() {
for component in self.components.lock().unwrap().iter_mut() {
if let Some(action) = component.handle_events(Some(event.clone()))? {
action_tx.send(action)?;
}
Expand Down Expand Up @@ -200,11 +177,14 @@ impl App {
Action::Suspend => self.should_suspend = true,
Action::Resume => self.should_suspend = false,
Action::ClearScreen => tui.terminal.clear()?,
Action::Resize(w, h) => self.handle_resize(tui, w, h)?,
Action::Render => self.render(tui)?,
// This needs to send a particular RenderingMessage to the rendering task
// in order to do:
// >> tui.resize(Rect::new(0, 0, w, h))?;
Action::Resize(w, h) => self.render_tx.send(RenderingTask::Resize(w, h))?,
Action::Render => self.render_tx.send(RenderingTask::Render)?,
_ => {}
}
for component in self.components.iter_mut() {
for component in self.components.lock().unwrap().iter_mut() {
if let Some(action) = component.update(action.clone())? {
self.action_tx.send(action)?
};
Expand All @@ -213,22 +193,53 @@ impl App {
Ok(())
}

fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> Result<()> {
tui.resize(Rect::new(0, 0, w, h))?;
self.render(tui)?;
Ok(())
}
async fn render(&mut self, tui: Tui) -> Result<()> {
let components = self.components.clone();
for component in components.lock().unwrap().iter_mut() {
component.register_action_handler(self.action_tx.clone());
}
for component in components.lock().unwrap().iter_mut() {
component.register_config_handler(self.config.clone());
}
for component in components.lock().unwrap().iter_mut() {
component.init(tui.size().unwrap());
}

fn render(&mut self, tui: &mut Tui) -> Result<()> {
tui.draw(|frame| {
for component in self.components.iter_mut() {
if let Err(err) = component.draw(frame, frame.area()) {
let _ = self
.action_tx
.send(Action::Error(format!("Failed to draw: {:?}", err)));
// Rendering loop
loop {
if let Some(task) = self.render_rx.recv().await {
match task {
RenderingTask::ClearScreen => {
tui.terminal.clear()?;
}
RenderingTask::Render => {
let mut c = components.lock().unwrap();
tui.terminal.draw(|frame| {
for component in c.iter_mut() {
if let Err(err) = component.draw(frame, frame.area()) {
let _ = self
.action_tx
.send(Action::Error(format!("Failed to draw: {:?}", err)));
}
}
});
}
RenderingTask::Resize(w, h) => {
tui.resize(Rect::new(0, 0, w, h))?;
let _ = self.action_tx.send(Action::Render);
}
RenderingTask::Quit => {
break Ok(());
}
}
}
})?;
Ok(())
}
}
}

enum RenderingTask {
ClearScreen,
Render,
Resize(u16, u16),
Quit,
}
2 changes: 1 addition & 1 deletion src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub mod home;
///
/// Implementors of this trait can be registered with the main application loop and will be able to
/// receive events, update state, and be rendered on the screen.
pub trait Component {
pub trait Component: Send {
/// Register an action handler that can send actions for processing if necessary.
///
/// # Arguments
Expand Down
10 changes: 10 additions & 0 deletions src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ impl Tui {
});
}

// TODO: I think we can decouple the event loop from the Tui struct to better
// separate responsibilities and avoid having to share Tui references between
// the event loop and the rendering loop.
//
// Goals:
// - Have a dedicated event loop independent of the Tui struct
// - Events should be directly translated into actions inside the event loop
// and then sent to the action handling loop
// - Have a dedicated rendering loop that can own the Tui struct
// Et voilà
async fn event_loop(
event_tx: UnboundedSender<Event>,
cancellation_token: CancellationToken,
Expand Down

0 comments on commit 52c48bb

Please sign in to comment.