Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finish Get started with Druid chapter of the book #2173

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ You can find its changes [documented below](#070---2021-01-01).

### Docs

- Finish `Get started with Druid` chapter of the book ([#2173] by [@LoipesMas])
- Fixed docs of derived Lens ([#1523] by [@Maan2003])
- Fixed docs describing `ViewSwitcher` widget functionality ([#1693] by [@arthmis])
- Added missing documentation on derived lens items ([#1696] by [@lidin])
Expand Down Expand Up @@ -553,6 +554,7 @@ Last release without a changelog :(
[@superfell]: https://github.com/superfell
[@GoldsteinE]: https://github.com/GoldsteinE
[@twitchyliquid64]: https://github.com/twitchyliquid64
[@LoipesMas]: https://github.com/LoipesMas

[#599]: https://github.com/linebender/druid/pull/599
[#611]: https://github.com/linebender/druid/pull/611
Expand Down Expand Up @@ -845,6 +847,7 @@ Last release without a changelog :(
[#2151]: https://github.com/linebender/druid/pull/2151
[#2157]: https://github.com/linebender/druid/pull/2157
[#2158]: https://github.com/linebender/druid/pull/2158
[#2173]: https://github.com/linebender/druid/pull/2173

[Unreleased]: https://github.com/linebender/druid/compare/v0.7.0...master
[0.7.0]: https://github.com/linebender/druid/compare/v0.6.0...v0.7.0
Expand Down
158 changes: 152 additions & 6 deletions docs/src/get_started.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
# Get started with Druid
*this is outdated, and should be replaced with a walkthrough of getting a simple
app built and running*.

This chapter will walk you through setting up a simple Druid application from start to finish.

## Set up a Druid project
Expand Down Expand Up @@ -83,17 +80,166 @@ Do not forget to import the new widgets;
use druid::widget::{Label, Flex, Padding, Align};
```

But this is can get too verbose, so there are helper functions, such as `center()`, `padding(x)`, etc.

They take the widget and wrap it into another widget for you. This makes it easy to chain different features, like this:
```rust, noplaypen
Label::new("foo").center().padding(5.0).fix_height(42.0).border(Color::RED, 5.0)
```

Here's how it would look like in our example:
```rust, noplaypen
fn build_ui() -> impl Widget<()> {
Flex::row()
.with_flex_child(
Flex::column()
.with_flex_child(Label::new("top left"), 1.0)
.with_flex_child(Label::new("bottom left").center(), 1.0),
1.0)
.with_flex_child(
Flex::column()
.with_flex_child(Label::new("top right"), 1.0)
.with_flex_child(Label::new("bottom right").center(), 1.0),
1.0)
.padding(10.0)
}
```
This does not require importing `Padding` or `Align`, but requires importing `druid::WidgetExt`.



## Application state
We can display a window and draw and position widgets in it. Now it's time to find out how we can tie these widgets to
the rest of our application. First lets see how we can display information from our application in the user interface.
For this we need to define what our application's state looks like.

...
```rust, noplaypen
use druid::Data;

#[derive(Data, Clone)]
struct AppState {
some_text: String,
}
```

Your application state struct needs to implement `Data` (which can be derived and requires `Clone`).
Members can be anything, but it's recommended to use types that already implement `Data`. Check [`Data` trait] section for more info.

Now we want to use our state to drive what gets displayed.
```rust, noplaypen
fn main() -> Result<(), PlatformError> {
let state = AppState {
label_text: String::from("String stored in AppState"),
};
AppLauncher::with_window(WindowDesc::new(build_ui())).launch(state)?;
Ok(())
}

fn build_ui() -> impl Widget<AppState> {
Flex::column()
.with_child(Label::dynamic(|data: &AppState, _env| data.label_text.clone()))
.padding(10.0)
}
```
In `main()` we initialize the state and pass it to the `AppLauncher`.

We changed the return type of the `build_ui` function. Now we say that our widget operates on data that is `AppState`. This struct is (usually) passed down to children of widgets in various functions.

Then, we create a dynamic `Label`, which takes a closure that gets a reference to the `AppState`.
In this closure we can do whatever we need and return a `String` that will be displayed in the `Label`.
Right now we just take the `label_text` member of `AppState` and clone it.

## Handle user input
We can't achieve much without modifying the app state.

Here we will use `TextBox` widget to get input from the user.

```rust, noplaypen
fn build_ui() -> impl Widget<AppState> {
Flex::column()
.with_child(Label::dynamic(|data: &AppState, _env| data.label_text.clone()))
.with_child(TextBox::new().lens(AppState::label_text))
.padding(10.0)
}
```

You might notice that we used something new here: `.lens()` function.

You can learn more about lenses in [`Lens` trait] section, but the general gist is that lenses allow you to easily select a subset of your app state to pass down to the widget. Because `TextBox` usually does not require to know everything about the state, just the `String` that needs to be displayed and updated.

...
But to use this functionality like that, we need to derive `Lens` on our `AppState`, so now it looks like this:
```rust, noplaypen
#[derive(Data, Clone, Lens)]
struct AppState {
label_text: String,
}
```
Remember to import `druid::Lens`.

Now user can edit the `label_text`!

## Putting it all together
Here's an example app that uses what we learned so far and greets the user.


```rust, noplaypen
#use druid::widget::prelude::*;
#use druid::widget::{Flex, Label, TextBox};
#use druid::{AppLauncher, Data, Lens, UnitPoint, WidgetExt, WindowDesc};
#
const VERTICAL_WIDGET_SPACING: f64 = 20.0;
const TEXT_BOX_WIDTH: f64 = 200.0;

#[derive(Clone, Data, Lens)]
struct HelloState {
name: String,
}

pub fn main() {
// describe the main window
let main_window = WindowDesc::new(build_ui())
.title("Hello World!")
.window_size((400.0, 400.0));

// create the initial app state
let initial_state: HelloState = HelloState {
name: "World".into(),
};

// start the application. Here we pass in the application state.
AppLauncher::with_window(main_window)
.log_to_console()
.launch(initial_state)
.expect("Failed to launch application");
}

fn build_ui() -> impl Widget<HelloState> {
// a label that will determine its text based on the current app data.
let label = Label::new(|data: &HelloState, _env: &Env| {
if data.name.is_empty() {
"Hello anybody!?".to_string()
} else {
format!("Hello {}!", data.name)
}
})
.with_text_size(32.0);

// a textbox that modifies `name`.
let textbox = TextBox::new()
.with_placeholder("Who are we greeting?")
.with_text_size(18.0)
.fix_width(TEXT_BOX_WIDTH)
.lens(HelloState::name);

// arrange the two widgets vertically, with some padding
Flex::column()
.with_child(label)
.with_spacer(VERTICAL_WIDGET_SPACING)
.with_child(textbox)
.align_vertical(UnitPoint::CENTER)
}
```


...
[`Data` trait]: data.md
[`Lens` trait]: lens.md