From 77aded8fff483c618d0028826a50a55557fa8572 Mon Sep 17 00:00:00 2001 From: Dheepak Krishnamurthy Date: Tue, 20 Feb 2024 22:15:39 -0500 Subject: [PATCH] feat: Update widgets section and conclusion --- astro.config.mjs | 1 + code/crates-tui-tutorial-app/src/app.rs | 57 ++++++++--------- .../src/widgets/search_page.rs | 12 ++-- src/content/docs/tutorials/crates-tui/app.md | 64 +++++++++++++++++++ .../docs/tutorials/crates-tui/conclusion.md | 7 +- .../crates-tui/crates-tui-demo-1.png | 4 +- .../crates-tui/crates-tui-demo-2.png | 4 +- .../tutorials/crates-tui/crates-tui-demo.gif | 4 +- .../docs/tutorials/crates-tui/search.md | 8 +-- .../docs/tutorials/crates-tui/widgets.md | 14 ++-- 10 files changed, 116 insertions(+), 59 deletions(-) create mode 100644 src/content/docs/tutorials/crates-tui/app.md diff --git a/astro.config.mjs b/astro.config.mjs index 382ab6b76..163201685 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -173,6 +173,7 @@ export default defineConfig({ { label: "Search", link: "/tutorials/crates-tui/search" }, { label: "Prompt", link: "/tutorials/crates-tui/prompt" }, { label: "Results", link: "/tutorials/crates-tui/results" }, + { label: "App", link: "/tutorials/crates-tui/app" }, ], }, { label: "Conclusion", link: "/tutorials/crates-tui/conclusion" }, diff --git a/code/crates-tui-tutorial-app/src/app.rs b/code/crates-tui-tutorial-app/src/app.rs index 253701352..fafd08220 100644 --- a/code/crates-tui-tutorial-app/src/app.rs +++ b/code/crates-tui-tutorial-app/src/app.rs @@ -1,13 +1,20 @@ +// ANCHOR: imports_all +// ANCHOR: imports_external use color_eyre::eyre::Result; use crossterm::event::KeyEvent; use ratatui::{prelude::*, widgets::Paragraph}; +// ANCHOR: imports_external +// ANCHOR: imports_core use crate::{ events::{Event, Events}, tui::Tui, widgets::{search_page::SearchPage, search_page::SearchPageWidget}, }; +// ANCHOR_END: imports_core +// ANCHOR_END: imports_all +// ANCHOR: full_app // ANCHOR: mode #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Mode { @@ -52,20 +59,16 @@ pub enum Action { } // ANCHOR_END: action -// ANCHOR: app_widget -struct AppWidget; -// ANCHOR_END: app_widget - // ANCHOR: app #[derive(Debug)] pub struct App { quit: bool, last_key_event: Option, mode: Mode, - rx: tokio::sync::mpsc::UnboundedReceiver, tx: tokio::sync::mpsc::UnboundedSender, - search_page: SearchPage, + + search_page: SearchPage, // new } // ANCHOR_END: app @@ -139,10 +142,12 @@ impl App { match action { Action::Quit => self.quit(), Action::SwitchMode(mode) => self.switch_mode(mode), - Action::ScrollUp => self.scroll_up(), - Action::ScrollDown => self.scroll_down(), - Action::SubmitSearchQuery => self.submit_search_query(), - Action::UpdateSearchResults => self.update_search_results(), + Action::ScrollUp => self.search_page.scroll_up(), + Action::ScrollDown => self.search_page.scroll_down(), + Action::SubmitSearchQuery => self.search_page.submit_query(), + Action::UpdateSearchResults => { + self.search_page.update_search_results() + } } Ok(()) } @@ -170,28 +175,10 @@ impl App { self.quit = true } - fn scroll_up(&mut self) { - self.search_page.scroll_up() - } - - fn scroll_down(&mut self) { - self.search_page.scroll_down() - } - fn switch_mode(&mut self, mode: Mode) { self.mode = mode; } - fn submit_search_query(&mut self) { - self.switch_mode(Mode::Results); - self.search_page.submit_query() - } - - fn update_search_results(&mut self) { - self.search_page.update_search_results(); - self.scroll_down(); - } - fn should_quit(&self) -> bool { self.quit } @@ -205,19 +192,27 @@ impl App { } } +// ANCHOR: app_widget +struct AppWidget; +// ANCHOR_END: app_widget + // ANCHOR: app_statefulwidget impl StatefulWidget for AppWidget { type State = App; fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { - let [last_key_event, search_page] = + let [status_bar, search_page] = Layout::vertical([Constraint::Length(1), Constraint::Fill(0)]) .areas(area); if let Some(key) = state.last_key_event { Paragraph::new(format!("last key event: {:?}", key.code)) .right_aligned() - .render(last_key_event, buf); + .render(status_bar, buf); + } + + if state.search_page.loading() { + Line::from("Loading...").render(status_bar, buf); } SearchPageWidget { mode: state.mode }.render( @@ -228,3 +223,5 @@ impl StatefulWidget for AppWidget { } } // ANCHOR_END: app_statefulwidget + +// ANCHOR_END: full_app diff --git a/code/crates-tui-tutorial-app/src/widgets/search_page.rs b/code/crates-tui-tutorial-app/src/widgets/search_page.rs index 6166a68d3..8576714b1 100644 --- a/code/crates-tui-tutorial-app/src/widgets/search_page.rs +++ b/code/crates-tui-tutorial-app/src/widgets/search_page.rs @@ -7,8 +7,7 @@ use crossterm::event::{Event as CrosstermEvent, KeyEvent}; use itertools::Itertools; use ratatui::{ layout::{Constraint, Layout, Position}, - text::Line, - widgets::{StatefulWidget, Widget}, + widgets::StatefulWidget, }; use tokio::sync::mpsc::UnboundedSender; use tui_input::backend::crossterm::EventHandler; @@ -81,10 +80,12 @@ impl SearchPage { self.crates.lock().unwrap().iter().cloned().collect_vec(); self.results.crates = crates; self.results.content_length(self.results.crates.len()); + self.scroll_down(); } // ANCHOR: submit pub fn submit_query(&mut self) { + let _ = self.tx.send(Action::SwitchMode(Mode::Results)); self.prepare_request(); self.request_search_results(); } @@ -108,7 +109,8 @@ impl SearchPage { // ANCHOR: request_search_results pub fn request_search_results(&self) { let loading_status = self.loading_status.clone(); - let params = self.create_search_parameters(); + let mut params = self.create_search_parameters(); + params.fake_delay = 5; tokio::spawn(async move { loading_status.store(true, Ordering::SeqCst); let _ = crates_io_api_helper::request_search_results(¶ms).await; @@ -134,10 +136,6 @@ impl StatefulWidget for SearchPageWidget { buf: &mut ratatui::prelude::Buffer, state: &mut Self::State, ) { - if state.loading() { - Line::from("Loading...").render(area, buf); - } - let prompt_height = 5; let [main, prompt] = Layout::vertical([ diff --git a/src/content/docs/tutorials/crates-tui/app.md b/src/content/docs/tutorials/crates-tui/app.md new file mode 100644 index 000000000..2d2445322 --- /dev/null +++ b/src/content/docs/tutorials/crates-tui/app.md @@ -0,0 +1,64 @@ +--- +title: App +--- + +Finally, let's make a field in the app struct that uses the `SearchPage` widget: + +```rust +{{#include @code/crates-tui-tutorial-app/src/app.rs:imports_core}} + +{{#include @code/crates-tui-tutorial-app/src/app.rs:app}} +``` + +With this refactor, now `./src/app.rs` becomes a lot simpler. For example, app now delegates to the +search page widget for all core functionality. + +```rust +impl App { +{{#include @code/crates-tui-tutorial-app/src/app.rs:app_handle_action}} +} +``` + +And rendering delegates to `SearchPageWidget`: + +```rust +impl App { +{{#include @code/crates-tui-tutorial-app/src/app.rs:app_statefulwidget}} +} +``` + +
+ +Copy the following into src/app.rs + +```rust +{{#include @code/crates-tui-tutorial-app/src/app.rs}} +``` + +
+ +Your final folder structure will look like this: + +``` +. +├── Cargo.lock +├── Cargo.toml +└── src + ├── app.rs + ├── crates_io_api_helper.rs + ├── errors.rs + ├── events.rs + ├── main.rs + ├── tui.rs + ├── widgets + │ ├── search_page.rs + │ ├── search_prompt.rs + │ └── search_results.rs + └── widgets.rs +``` + +If you put all of it together, you should be able run the TUI. + +![](./crates-tui-demo.gif) + +Search for your favorite crates and explore crates.io. diff --git a/src/content/docs/tutorials/crates-tui/conclusion.md b/src/content/docs/tutorials/crates-tui/conclusion.md index 6cac3925c..e6ea1c138 100644 --- a/src/content/docs/tutorials/crates-tui/conclusion.md +++ b/src/content/docs/tutorials/crates-tui/conclusion.md @@ -2,12 +2,11 @@ title: Conclusion --- -If you put all of it together, you should be able run the TUI. +Congratulations! :tada: -![](./crates-tui-demo.gif) +You built your first `async` TUI application. -We only touched on the basics for building an `async` application with Ratatui, using `tokio` and -`crossterm`'s async features. +![](./crates-tui-demo.gif) If you are interested in learning more, check out the source code for [`crates-tui`] for a more complex and featureful version of this tutorial. diff --git a/src/content/docs/tutorials/crates-tui/crates-tui-demo-1.png b/src/content/docs/tutorials/crates-tui/crates-tui-demo-1.png index 09721ab68..043dd9cf8 100644 --- a/src/content/docs/tutorials/crates-tui/crates-tui-demo-1.png +++ b/src/content/docs/tutorials/crates-tui/crates-tui-demo-1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbaf8339d7b1a053ac608385daacc9e66e5ce3a64df427fc9826ea156e1bb145 -size 260146 +oid sha256:30c0a4665ab8b8b6d4d3c3968e1d3a2702d5e10f0cde5ac87e8008089843b75b +size 259785 diff --git a/src/content/docs/tutorials/crates-tui/crates-tui-demo-2.png b/src/content/docs/tutorials/crates-tui/crates-tui-demo-2.png index fb7eb039a..8beb846b5 100644 --- a/src/content/docs/tutorials/crates-tui/crates-tui-demo-2.png +++ b/src/content/docs/tutorials/crates-tui/crates-tui-demo-2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7efb196226d8607eed4485d3bab43932f7157245f396c6181f59c4b0a0ea00e -size 258110 +oid sha256:091e3ceb4c222c9a87bdd7a11aef1317668e2fd54bfc05d9e35ca525d07b1f9a +size 257693 diff --git a/src/content/docs/tutorials/crates-tui/crates-tui-demo.gif b/src/content/docs/tutorials/crates-tui/crates-tui-demo.gif index 87353f5ed..142916337 100644 --- a/src/content/docs/tutorials/crates-tui/crates-tui-demo.gif +++ b/src/content/docs/tutorials/crates-tui/crates-tui-demo.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffea29bc139df5f60b1c3ccf7ea4d5bfcf25ff6a2cfdbfbc7f2987e868b5dbe4 -size 281231 +oid sha256:e0a7f6010e30afa649a0b598c61a3ccd2a01a90b81d3e0a7bdb07bfda0d9bf37 +size 243417 diff --git a/src/content/docs/tutorials/crates-tui/search.md b/src/content/docs/tutorials/crates-tui/search.md index 8447ffb5f..ba268d11a 100644 --- a/src/content/docs/tutorials/crates-tui/search.md +++ b/src/content/docs/tutorials/crates-tui/search.md @@ -2,13 +2,7 @@ title: Search --- -Let's make a field in the app struct called `SearchPage`: - -```rust -{{#include @code/crates-tui-tutorial-app/src/app.rs:app}} -``` - -And we can create a new file, `./src/widgets/search_page.rs` with the following contents: +Create a new file, `./src/widgets/search_page.rs` with the following contents: ```rust {{#include @code/crates-tui-tutorial-app/src/widgets/search_page.rs:search_page}} diff --git a/src/content/docs/tutorials/crates-tui/widgets.md b/src/content/docs/tutorials/crates-tui/widgets.md index a7039a34f..7f724e31d 100644 --- a/src/content/docs/tutorials/crates-tui/widgets.md +++ b/src/content/docs/tutorials/crates-tui/widgets.md @@ -2,7 +2,9 @@ title: Widgets --- -In this section we will discuss the widgets implemented for this tutorial: +In this section we will discuss implementing widgets. + +Create a new file `./src/widgets.rs` with the following content: ```rust {{#include @code/crates-tui-tutorial-app/src/widgets.rs}} @@ -13,8 +15,9 @@ widget. ![](./crates-tui-demo-1.png) -For the `SearchResults`, we will use a `Table` and a `Scrollbar` widget. For the `SearchPrompt`, we -will use a `Block` with borders and `Paragraph`s for the text. +For the `SearchResults`, we will use a `Table` like before, and additionally a `Scrollbar` widget. +For the `SearchPrompt`, we will use a `Block` with borders and `Paragraph`s for the text like +before. We will be using the `StatefulWidget` pattern. `StatefulWidget` is a trait in Ratatui that is defined like so: @@ -31,5 +34,6 @@ For this `StatefulWidget` pattern, you will always have at a minimum two `struct 1. the state 2. the widget -We used this pattern in the `app` module with the `App` struct as the state and the `AppWidget` -struct as the widget that is rendered. +You used this pattern already in the `app` module with the `App` struct as the state and the +`AppWidget` struct as the widget that is rendered. Now you are going to apply it to refactor the +`App` into children.