diff --git a/Readme.md b/Readme.md index 8c87cc0..81e2376 100644 --- a/Readme.md +++ b/Readme.md @@ -63,8 +63,9 @@ _(Colors may depend on your terminal configuration.)_ These tutorials may help you get started with cursive: -* [Starting with cursive: Basics](https://github.com/gyscos/Cursive/tree/master/doc/tutorial_1.md) -* [Starting with cursive: Intermediate](https://github.com/gyscos/Cursive/tree/master/doc/tutorial_2.md) +* [Starting with cursive: (1/3)](https://github.com/gyscos/Cursive/tree/master/doc/tutorial_1.md) +* [Starting with cursive: (2/3)](https://github.com/gyscos/Cursive/tree/master/doc/tutorial_2.md) +* [Starting with cursive: (3/3)](https://github.com/gyscos/Cursive/tree/master/doc/tutorial_3.md) ## Goals diff --git a/doc/tutorial_1.md b/doc/tutorial_1.md index e967234..7083966 100644 --- a/doc/tutorial_1.md +++ b/doc/tutorial_1.md @@ -1,4 +1,4 @@ -# Starting with cursive: Basics +# Starting with cursive: (1/3) ## Target goal diff --git a/doc/tutorial_2.md b/doc/tutorial_2.md index 6f20468..4b49200 100644 --- a/doc/tutorial_2.md +++ b/doc/tutorial_2.md @@ -1,4 +1,4 @@ -# Starting with cursive: Intermediate +# Starting with cursive (2/3) ## Target goal diff --git a/doc/tutorial_3.md b/doc/tutorial_3.md new file mode 100644 index 0000000..e8830ee --- /dev/null +++ b/doc/tutorial_3.md @@ -0,0 +1,314 @@ +# Starting with cursive (3/3) + +## Target goal + +In this part, we'll introduce some mutability. Until now, we were adding +elements to the UI, and then we'd forget about them. This time, we'll remember +them to update them. + +Here is the code we'll end up with: + +```rust +extern crate cursive; + +use cursive::prelude::*; + +fn main() { + let mut siv = Cursive::new(); + + let select = SelectView::::new() + .on_submit(on_submit) + .with_id("select") + .fixed_size((10, 5)); + let buttons = LinearLayout::vertical() + .child(Button::new("Add new", add_name)) + .child(Button::new("Delete", delete_name)) + .child(DummyView) + .child(Button::new("Quit", Cursive::quit)); + + siv.add_layer(Dialog::new(LinearLayout::horizontal() + .child(select) + .child(DummyView) + .child(buttons)) + .title("Select a profile")); + + siv.run(); +} + +fn add_name(s: &mut Cursive) { + fn ok(s: &mut Cursive, name: &str) { + s.find_id::>("select").unwrap().add_item_str(name); + s.pop_layer(); + } + + s.add_layer(Dialog::new(EditView::new() + .on_submit(ok) + .with_id("name") + .fixed_width(10)) + .title("Enter a new name") + .button("Ok", |s| { + let name = + s.find_id::("name").unwrap().get_content().clone(); + ok(s, &name); + }) + .button("Cancel", |s| s.pop_layer())); +} + +fn delete_name(s: &mut Cursive) { + match s.find_id::>("select").unwrap().selected_id() { + None => s.add_layer(Dialog::info("No name to remove")), + Some(focus) => { + s.find_id::>("select") + .unwrap() + .remove_item(focus) + } + } +} + +fn on_submit(s: &mut Cursive, name: &String) { + s.pop_layer(); + s.add_layer(Dialog::text(format!("Name: {}\nAwesome: yes", name)) + .title(format!("{}'s info", name)) + .button("Quit", Cursive::quit)); +} +``` + +![Tutorial 3 goal](../doc/cursive_example.png) + +## SelectView + +The main element in our application will be a list of names. For this, we'll +use a [`SelectView`]. This type is generic on the item stored. We just want +to store the names, so let's build a `SelectView`: + +```rust +let select = SelectView::::new(); +``` + +Our list will start empty. If we leave it like that, it will be tiny when +the application starts, and will grow when we add names. This is not very +professional-looking, so we'll give it a fixed size. + +To do that, a [`BoxView`] can wrap any view and give it a fixed size. +We could do: + +```rust +let select = BoxView::with_fixed_size((10, 5), SelectView::::new()); +``` + +But there is another shorter way: the [`Boxable`] trait is conveniently +implemented for any `View`, and allow to wrap in a `BoxView` with a chainable +call: + +```rust +let select = SelectView::::new() + .fixed_size((10, 5)); +``` + +We'll also want to add a callback when the user chooses a name. The +[`SelectView::on_submit`] method takes a callback with a second argument: + the selected item. Since we're using `String`, our callback will have to be + `Fn(&mut Cursive, &String)`: + +```rust +let select = SelectView::::new() + .on_submit(on_submit) + .fixed_size((10, 5)); + +fn on_submit(s: &mut Cursive, name: &String) { + s.pop_layer(); + s.add_layer(Dialog::text(format!("Name: {}\nAwesome: yes", name)) + .title(format!("{}'s info", name)) + .button("Quit", Cursive::quit)); +} +``` + +(Be sure to call `on_submit` on the `SelectView`, +not on the `BoxView` returned by `fixed_size`!) + +What we do there should be pretty familiar by now: +replace the layer with a simple dialog. + +[`SelectView`]: http://gyscos.github.io/Cursive/cursive/views/struct.SelectView.html +[`BoxView`]: http://gyscos.github.io/Cursive/cursive/views/struct.BoxView.html +[`Boxable`]: http://gyscos.github.io/Cursive/cursive/view/trait.Boxable.html +[`SelectView::on_submit`]: http://gyscos.github.io/Cursive/cursive/views/struct.SelectView.html#method.on_submit + +## Linear layouts + +Our main screen is more complex than our previous examples: it is made of +several views. There is a `SelectView` on the left, and three [`Button`]s to +the right. But our [`Dialog::new`] method only takes one view! How will we do? + +The solution is to use a layout view to display multiple children side-by-side. +[`LinearLayout`] for instance can display views in a line. +We'll use two of them: + +* One, set vertically, will hold the buttons on the right. +* Another one, horizontally, will contain the list and the vertical layout. + +Let's start with the column of buttons: + +```rust +let buttons = LinearLayout::vertical() + .child(Button::new("Add new", add_name)) + .child(Button::new("Delete", delete_name)) + .child(DummyView) + .child(Button::new("Quit", Cursive::quit)); + +fn add_name(s: &mut Cursive) {} +fn delete_name(s: &mut Cursive) {} +``` + +Buttons here take a regular callback. We're also adding a [`DummyView`] here: +this view doesn't do anything, it just occupies a single row - we're using it +as a cheap spacer. + +We can now create the second linear layout inside a Dialog: + +```rust +siv.add_layer(Dialog::new(LinearLayout::horizontal() + .child(select) + .child(DummyView) + .child(buttons)) + .title("Select a profile")); +``` + +We've added a `DummyView` again to add some space between the list and the +buttons. Though with an empty list, it doesn't look like much yet. Let's fill +this list with names! + +[`Button`]: http://gyscos.github.io/Cursive/cursive/views/struct.Button.html +[`Dialog::new`]: http://gyscos.github.io/Cursive/cursive/views/struct.Dialog.html#method.new +[`LinearLayout`]: http://gyscos.github.io/Cursive/cursive/views/struct.LinearLayout.html +[`DummyView`]: http://gyscos.github.io/Cursive/cursive/views/struct.DummyView.html + +## IDs + +When the user presses the `` button, we want to show a popup where he +can enter a new name: + +```rust +fn add_name(s: &mut Cursive) { + s.add_layer(Dialog::new(EditView::new() + .fixed_width(10)) + .title("Enter a new name") + .button("Ok", |s| { + // What do we do now?... + }) + .button("Cancel", |s| s.pop_layer())); +} +``` + +We're using [`EditView`] here, that we wrap in a fixed width like we did with +the `SelectView` earlier. We have an `` button, but... what do we do there? + +We need to fetch the content of the `EditView` from the callback, but we don't +have a reference to it. And if you try to declare a variable for the `EditView` +and use it in the closure, you'll soon be submerged by compiler errors, for +good reasons. But there is a way! + +The closure has access to the `&mut Cursive`, which in turn has access to all +the views, so _in theory_, we could ask it to borrow the view, if only we knew +how to point to the correct view. + +[`IdView`] is meant exactly for this: it wraps a view and gives it an ID. +Later, you can ask the Cursive root for this ID and get access to the view. +Just what we need! + +Like `BoxView`, `IdView` can be used directly with [`IdView::new`], or through +the [`Identifiable`] trait. [`Cursive::find_id`] can then give you a mutable +reference to the view. + +Here's what it looks like in action: + +```rust +fn add_name(s: &mut Cursive) { + s.add_layer(Dialog::new(EditView::new() + .with_id("name") + .fixed_width(10)) + .title("Enter a new name") + .button("Ok", |s| { + let name = + s.find_id::("name").unwrap().get_content().clone(); + }) + .button("Cancel", |s| s.pop_layer())); +} +``` + +We create the `EditView` with the id `"name"`, and we use `"name"` again when +calling `find_id`. + +Now we just need to do something with this name: add it to the list! +Remember the `SelectView` we created? Let's give it an ID too: + +```rust +let select = SelectView::::new() + .on_submit(on_submit) + .with_id("select") + .fixed_size((10, 5)); +``` +(Here again, the order is important: we want to wrap the `SelectView`, not +the `BoxView`. But we still need to call `on_submit` before that.) + +That way, we can update it with a new item: + +```rust +fn add_name(s: &mut Cursive) { + fn ok(s: &mut Cursive, name: &str) { + s.find_id::>("select").unwrap().add_item_str(name); + s.pop_layer(); + } + + s.add_layer(Dialog::new(EditView::new() + .on_submit(ok) + .with_id("name") + .fixed_width(10)) + .title("Enter a new name") + .button("Ok", |s| { + let name = + s.find_id::("name").unwrap().get_content().clone(); + ok(s, &name); + }) + .button("Cancel", |s| s.pop_layer())); +} +``` + +Now that we know how to access the `SelectView`, removing an item is not very +complicated: + +```rust +fn delete_name(s: &mut Cursive) { + match s.find_id::>("select").unwrap().selected_id() { + None => s.add_layer(Dialog::info("No name to remove")), + Some(focus) => { + s.find_id::>("select") + .unwrap() + .remove_item(focus) + } + } +} +``` + +We use [`SelectView::selected_id`] and [`SelectView::remove_item`] to remove +the item currently selected, nothing too surprising. +We have to find the `SelectView` twice, otherwise we're still borrowing `s` +when we try to add a new layer - one of the quirks of the borrow checker. + +[`IdView`]: http://gyscos.github.io/Cursive/cursive/views/struct.IdView.html +[`IdView::new`]: http://gyscos.github.io/Cursive/cursive/prelude/struct.IdView.html#method.new +[`Identifiable`]: http://gyscos.github.io/Cursive/cursive/view/trait.Identifiable.html +[`Cursive::find_id`]: http://gyscos.github.io/Cursive/cursive/struct.Cursive.html#method.find_id +[`SelectView::selected_id`]: http://gyscos.github.io/Cursive/cursive/views/struct.SelectView.html#method.selected_id +[`SelectView::remove_item`]: http://gyscos.github.io/Cursive/cursive/views/struct.SelectView.html#method.remove_item + +## Conclusion + +This tutorial was a bit larger than the previous ones, so take your time, and +don't hesitate to read the documentation. + +You've now seen: +* How to wrap views to control their size +* How to assemble views together in a linear layout +* How to give an ID to views and use them later +* How to use `SelectView`, `EditView`, `Button`s...