Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 32 additions & 20 deletions code/tutorials/json-editor/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,56 +9,68 @@ pub enum CurrentScreen {
}
// ANCHOR_END: screen_modes

// ANCHOR: currently_editing
pub enum CurrentlyEditing {
// ANCHOR: edit_focus
#[derive(Clone, Copy)]
pub enum EditFocus {
Key,
Value,
}
// ANCHOR_END: currently_editing
// ANCHOR_END: edit_focus

// ANCHOR: editing_pair
pub struct EditingPair {
pub key: String, // the currently being edited json key.
pub value: String, // the currently being edited json value.
pub focus: EditFocus, // which field the user is editing.
}
// ANCHOR_END: editing_pair

// ANCHOR: app_fields
pub struct App {
pub key_input: String, // the currently being edited json key.
pub value_input: String, // the currently being edited json value.
pub pairs: HashMap<String, String>, // The representation of our key and value pairs with serde Serialize support
pub current_screen: CurrentScreen, // the current screen the user is looking at, and will later determine what is rendered.
pub currently_editing: Option<CurrentlyEditing>, // the optional state containing which of the key or value pair the user is editing. It is an option, because when the user is not directly editing a key-value pair, this will be set to `None`.
pub editing_pair: Option<EditingPair>, // the optional key-value pair currently being edited. It is an option, because when the user is not directly editing a key-value pair, this will be set to `None`.
}
// ANCHOR_END: app_fields

// ANCHOR: impl_new
impl App {
pub fn new() -> App {
App {
key_input: String::new(),
value_input: String::new(),
pairs: HashMap::new(),
current_screen: CurrentScreen::Main,
currently_editing: None,
editing_pair: None,
}
}
// ANCHOR_END: impl_new

// ANCHOR: start_editing
pub fn start_editing(&mut self) {
self.editing_pair = Some(EditingPair {
key: String::new(),
value: String::new(),
focus: EditFocus::Key,
});
}
// ANCHOR_END: start_editing

// ANCHOR: save_key_value
pub fn save_key_value(&mut self) {
self.pairs
.insert(self.key_input.clone(), self.value_input.clone());

self.key_input = String::new();
self.value_input = String::new();
self.currently_editing = None;
if let Some(editing_pair) = self.editing_pair.take() {
self.pairs.insert(editing_pair.key, editing_pair.value);
}
}
// ANCHOR_END: save_key_value

// ANCHOR: toggle_editing
pub fn toggle_editing(&mut self) {
if let Some(edit_mode) = &self.currently_editing {
match edit_mode {
CurrentlyEditing::Key => self.currently_editing = Some(CurrentlyEditing::Value),
CurrentlyEditing::Value => self.currently_editing = Some(CurrentlyEditing::Key),
if let Some(editing_pair) = &mut self.editing_pair {
match editing_pair.focus {
EditFocus::Key => editing_pair.focus = EditFocus::Value,
EditFocus::Value => editing_pair.focus = EditFocus::Key,
};
} else {
self.currently_editing = Some(CurrentlyEditing::Key);
self.start_editing();
}
}
// ANCHOR_END: toggle_editing
Expand Down
40 changes: 20 additions & 20 deletions code/tutorials/json-editor/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use ratatui::{
mod app;
mod ui;
use crate::{
app::{App, CurrentScreen, CurrentlyEditing},
app::{App, CurrentScreen, EditFocus},
ui::ui,
};

Expand Down Expand Up @@ -80,7 +80,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<
CurrentScreen::Main => match key.code {
KeyCode::Char('e') => {
app.current_screen = CurrentScreen::Editing;
app.currently_editing = Some(CurrentlyEditing::Key);
app.start_editing();
}
KeyCode::Char('q') => {
app.current_screen = CurrentScreen::Exiting;
Expand All @@ -103,12 +103,12 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<
CurrentScreen::Editing if key.kind == KeyEventKind::Press => {
match key.code {
KeyCode::Enter => {
if let Some(editing) = &app.currently_editing {
match editing {
CurrentlyEditing::Key => {
app.currently_editing = Some(CurrentlyEditing::Value);
if let Some(editing_pair) = &mut app.editing_pair {
match editing_pair.focus {
EditFocus::Key => {
editing_pair.focus = EditFocus::Value;
}
CurrentlyEditing::Value => {
EditFocus::Value => {
app.save_key_value();
app.current_screen = CurrentScreen::Main;
}
Expand All @@ -118,13 +118,13 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<
// ANCHOR_END: editing_enter
// ANCHOR: backspace_editing
KeyCode::Backspace => {
if let Some(editing) = &app.currently_editing {
match editing {
CurrentlyEditing::Key => {
app.key_input.pop();
if let Some(editing_pair) = &mut app.editing_pair {
match editing_pair.focus {
EditFocus::Key => {
editing_pair.key.pop();
}
CurrentlyEditing::Value => {
app.value_input.pop();
EditFocus::Value => {
editing_pair.value.pop();
}
}
}
Expand All @@ -133,7 +133,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<
// ANCHOR: escape_editing
KeyCode::Esc => {
app.current_screen = CurrentScreen::Main;
app.currently_editing = None;
app.editing_pair = None;
}
// ANCHOR_END: escape_editing
// ANCHOR: tab_editing
Expand All @@ -143,13 +143,13 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<
// ANCHOR_END: tab_editing
// ANCHOR: character_editing
KeyCode::Char(value) => {
if let Some(editing) = &app.currently_editing {
match editing {
CurrentlyEditing::Key => {
app.key_input.push(value);
if let Some(editing_pair) = &mut app.editing_pair {
match editing_pair.focus {
EditFocus::Key => {
editing_pair.key.push(value);
}
CurrentlyEditing::Value => {
app.value_input.push(value);
EditFocus::Value => {
editing_pair.value.push(value);
}
}
}
Expand Down
22 changes: 11 additions & 11 deletions code/tutorials/json-editor/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use ratatui::{
Frame,
};

use crate::app::{App, CurrentScreen, CurrentlyEditing};
use crate::app::{App, CurrentScreen, EditFocus};

// ANCHOR: method_sig
pub fn ui(frame: &mut Frame, app: &App) {
Expand Down Expand Up @@ -66,12 +66,12 @@ pub fn ui(frame: &mut Frame, app: &App) {
Span::styled(" | ", Style::default().fg(Color::White)),
// The final section of the text, with hints on what the user is editing
{
if let Some(editing) = &app.currently_editing {
match editing {
CurrentlyEditing::Key => {
if let Some(editing_pair) = &app.editing_pair {
match editing_pair.focus {
EditFocus::Key => {
Span::styled("Editing Json Key", Style::default().fg(Color::Green))
}
CurrentlyEditing::Value => {
EditFocus::Value => {
Span::styled("Editing Json Value", Style::default().fg(Color::LightGreen))
}
}
Expand Down Expand Up @@ -120,7 +120,7 @@ pub fn ui(frame: &mut Frame, app: &App) {
// ANCHOR_END: lower_navigation_rendering

// ANCHOR: editing_popup
if let Some(editing) = &app.currently_editing {
if let Some(editing_pair) = &app.editing_pair {
let popup_block = Block::default()
.title("Enter a new key-value pair")
.borders(Borders::NONE)
Expand All @@ -144,15 +144,15 @@ pub fn ui(frame: &mut Frame, app: &App) {

let active_style = Style::default().bg(Color::LightYellow).fg(Color::Black);

match editing {
CurrentlyEditing::Key => key_block = key_block.style(active_style),
CurrentlyEditing::Value => value_block = value_block.style(active_style),
match editing_pair.focus {
EditFocus::Key => key_block = key_block.style(active_style),
EditFocus::Value => value_block = value_block.style(active_style),
};

let key_text = Paragraph::new(app.key_input.clone()).block(key_block);
let key_text = Paragraph::new(editing_pair.key.as_str()).block(key_block);
frame.render_widget(key_text, popup_chunks[0]);

let value_text = Paragraph::new(app.value_input.clone()).block(value_block);
let value_text = Paragraph::new(editing_pair.value.as_str()).block(value_block);
frame.render_widget(value_text, popup_chunks[1]);
}
// ANCHOR_END: key_value_blocks
Expand Down
45 changes: 34 additions & 11 deletions src/content/docs/tutorials/json-editor/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ listen for.
We will be using the application's state to track two things:

1. what screen the user is seeing,
2. which box should be highlighted, the "key" or "value" (this only applies when the user is editing
a key-value pair).
1. the key-value pair currently being edited, including which box should be highlighted.

### Current Screen Enum

Expand All @@ -42,7 +41,7 @@ We represent these possible modes with a simple enum:
{{#include @code/tutorials/json-editor/src/app.rs:screen_modes}}
```

### Currently Editing Enum
### Edit Focus Enum

As you may already know, `ratatui` does not automatically redraw the screen[^note]. `ratatui` also
does not remember anything about what it drew last frame.
Expand All @@ -51,13 +50,26 @@ This means that the programmer is responsible for handling all state and updatin
changes. In this case, we will allow the user to input two strings in the `Editing` mode - a key and
a value. The programmer is responsible for knowing which the user is trying to edit.

For this purpose, we will create another enum for our application state called `CurrentlyEditing` to
keep track of which field the user is currently entering:
For this purpose, we will create another enum for our application state called `EditFocus` to keep
track of which field the user is currently entering:

```rust
{{#include @code/tutorials/json-editor/src/app.rs:currently_editing}}
{{#include @code/tutorials/json-editor/src/app.rs:edit_focus}}
```

### Editing Pair Struct

The key and value fields are only useful while the user is editing a new pair. Instead of storing
those temporary strings directly on `App`, we group them in an `EditingPair` with the field that is
currently active.

```rust
{{#include @code/tutorials/json-editor/src/app.rs:editing_pair}}
```

This keeps the draft input together: when no pair is being edited, the application does not need to
hold empty key and value strings.

## The full application state

Now that we have enums to help us track where the user is, we will create the struct that actually
Expand All @@ -84,11 +96,22 @@ change universal defaults for the state.
// --snip--
```

### `start_editing()`

When the user chooses to create a new pair, we create a fresh `EditingPair`. The key and value start
empty, and the cursor starts in the key field.

```rust
// --snip--
{{#include @code/tutorials/json-editor/src/app.rs:start_editing}}
// --snip--
```

### `save_key_value()`

This function will be called when the user saves a key-value pair in the editor. It adds the two
stored variables to the key-value pairs `HashMap`, and resets the status of all of the editing
variables.
This function will be called when the user saves a key-value pair in the editor. The `take()` call
moves the current `EditingPair` out of `App` and leaves `None` behind. We can then move the owned
key and value strings into the key-value pairs `HashMap` without cloning them.

```rust
// --snip--
Expand All @@ -100,8 +123,8 @@ variables.

Sometimes it is easier to put simple logic into a convenience function so we don't have to worry
about it in the main code block. `toggle_editing` is one of those cases. All we are doing, is
checking if something is currently being edited, and if it is, swapping between editing the Key and
Value fields.
checking if a pair is currently being edited, and if it is, swapping between editing the Key and
Value fields. If editing has not started yet, the helper creates a fresh `EditingPair`.

```rust
// --snip--
Expand Down
17 changes: 9 additions & 8 deletions src/content/docs/tutorials/json-editor/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,9 @@ We will start with the keybinds and event handling for the `CurrentScreen::Main`
After matching to the `Main` enum variant, we match the event. When the user is in the main screen,
there are only two keybinds, and the rest are ignored.

In this case, `KeyCode::Char('e')` changes the current screen to `CurrentScreen::Editing` and sets
the `CurrentlyEditing` to a `Some` and notes that the user should be editing the `Key` value field,
as opposed to the `Value` field.
In this case, `KeyCode::Char('e')` changes the current screen to `CurrentScreen::Editing` and calls
`start_editing()` to create a fresh key-value pair draft. The draft starts with the key field
selected, as opposed to the `Value` field.

`KeyCode::Char('q')` is straightforward, as it simply switches the application to the `Exiting`
screen, and allows the ui and future event handling runs to do the rest.
Expand Down Expand Up @@ -220,16 +220,17 @@ currently edited, `Enter` will save the key-value pair, and return to the `Main`
// --snip--
```

When `Backspace` is pressed, we need to first determine if the user is editing a `Key` or a `Value`,
then `pop()` the endings of those strings accordingly.
When `Backspace` is pressed, we first get the pair currently being edited, then determine if the
user is editing a `Key` or a `Value`. Once we know which field is active, we `pop()` the end of that
string.

```rust
// --snip--
{{#include @code/tutorials/json-editor/src/main.rs:backspace_editing}}
// --snip--
```

When `Escape` is pressed, we want to quit editing.
When `Escape` is pressed, we want to quit editing and discard the draft pair.

```rust
// --snip--
Expand All @@ -245,8 +246,8 @@ When `Tab` is pressed, we want the currently editing selection to switch.
// --snip--
```

And finally, if the user types a valid character, we want to capture that, and add it to the string
that is the final key or value.
And finally, if the user types a valid character, we want to capture that and add it to the draft's
key or value string.

```rust
// --snip--
Expand Down
Loading