Skip to content

Commit

Permalink
feat: Update widgets section and conclusion
Browse files Browse the repository at this point in the history
  • Loading branch information
kdheepak committed Feb 21, 2024
1 parent 2530ba0 commit 77aded8
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 59 deletions.
1 change: 1 addition & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
57 changes: 27 additions & 30 deletions code/crates-tui-tutorial-app/src/app.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<crossterm::event::KeyEvent>,
mode: Mode,

rx: tokio::sync::mpsc::UnboundedReceiver<Action>,
tx: tokio::sync::mpsc::UnboundedSender<Action>,
search_page: SearchPage,

search_page: SearchPage, // new
}
// ANCHOR_END: app

Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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(
Expand All @@ -228,3 +223,5 @@ impl StatefulWidget for AppWidget {
}
}
// ANCHOR_END: app_statefulwidget

// ANCHOR_END: full_app
12 changes: 5 additions & 7 deletions code/crates-tui-tutorial-app/src/widgets/search_page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand All @@ -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(&params).await;
Expand All @@ -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([
Expand Down
64 changes: 64 additions & 0 deletions src/content/docs/tutorials/crates-tui/app.md
Original file line number Diff line number Diff line change
@@ -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}}
}
```

<details>

<summary>Copy the following into <code>src/app.rs</code></summary>

```rust
{{#include @code/crates-tui-tutorial-app/src/app.rs}}
```

</details>

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.
7 changes: 3 additions & 4 deletions src/content/docs/tutorials/crates-tui/conclusion.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/tutorials/crates-tui/crates-tui-demo-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/content/docs/tutorials/crates-tui/crates-tui-demo-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/content/docs/tutorials/crates-tui/crates-tui-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 1 addition & 7 deletions src/content/docs/tutorials/crates-tui/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
14 changes: 9 additions & 5 deletions src/content/docs/tutorials/crates-tui/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand All @@ -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:
Expand All @@ -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.

0 comments on commit 77aded8

Please sign in to comment.