From f88b1dd29d4c448e3e99d082387001b57cb67d53 Mon Sep 17 00:00:00 2001 From: LoipesMas <46327403+LoipesMas@users.noreply.github.com> Date: Mon, 25 Apr 2022 23:56:22 +0200 Subject: [PATCH] Finish `Get started with Druid` chapter of the book --- CHANGELOG.md | 3 + docs/src/get_started.md | 158 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 155 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49df2b4eff..6783349406 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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]) @@ -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 @@ -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 diff --git a/docs/src/get_started.md b/docs/src/get_started.md index 75c1fdde20..501592b4e7 100644 --- a/docs/src/get_started.md +++ b/docs/src/get_started.md @@ -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 @@ -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 { + 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 { + 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 { + // 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