From 2ef912496c0bceafc7a40456396f0e2e6402d82f Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Sun, 12 Apr 2020 15:38:57 -0400 Subject: [PATCH 1/3] Add docs for Painter & Controller This is the first part of the major section going over custom widgets and the general widget model. --- docs/book_examples/src/custom_widgets_md.rs | 60 +++++++++++++ docs/book_examples/src/lib.rs | 1 + docs/src/custom_widgets.md | 94 +++++++++++++++++++-- 3 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 docs/book_examples/src/custom_widgets_md.rs diff --git a/docs/book_examples/src/custom_widgets_md.rs b/docs/book_examples/src/custom_widgets_md.rs new file mode 100644 index 0000000000..e6b21044dd --- /dev/null +++ b/docs/book_examples/src/custom_widgets_md.rs @@ -0,0 +1,60 @@ +use druid::widget::{Controller, Label, Painter, SizedBox, TextBox}; +use druid::{Color, Env, Event, EventCtx, KeyCode, PaintCtx, RenderContext, Widget, WidgetExt}; + +const CORNER_RADIUS: f64 = 4.0; +const STROKE_WIDTH: f64 = 2.0; + +// ANCHOR: color_swatch +fn make_color_swatch() -> Painter { + Painter::new(|ctx: &mut PaintCtx, data: &Color, env: &Env| { + let bounds = ctx.size().to_rect(); + let rounded = bounds.to_rounded_rect(CORNER_RADIUS); + ctx.fill(rounded, data); + ctx.stroke(rounded, &env.get(druid::theme::PRIMARY_DARK), STROKE_WIDTH); + }) +} +// ANCHOR_END: color_swatch + +// ANCHOR: sized_swatch +fn sized_swatch() -> impl Widget { + SizedBox::new(make_color_swatch()).width(20.0).height(20.0) +} +// ANCHOR_END: sized_swatch + +// ANCHOR: background_label +fn background_label() -> impl Widget { + Label::dynamic(|color: &Color, _| { + let (r, g, b, _) = color.as_rgba_u8(); + format!("#{:X}{:X}{:X}", r, g, b) + }) + .background(make_color_swatch()) +} +// ANCHOR_END: background_label + +// ANCHOR: annoying_textbox +#[derive(Default)] +struct AnnoyingController { + suppress_next: bool, +} + +impl Controller for AnnoyingController { + fn event( + &mut self, + child: &mut TextBox, + ctx: &mut EventCtx, + event: &Event, + data: &mut String, + env: &Env, + ) { + if matches!(event, Event::KeyDown(k) if k.key_code == KeyCode::Backspace) { + self.suppress_next = !self.suppress_next; + if self.suppress_next { + return; + } + } + + // if we want our child to receive this event, we must send it explicitly. + child.event(ctx, event, data, env); + } +} +// ANCHOR_END: annoying_textbox diff --git a/docs/book_examples/src/lib.rs b/docs/book_examples/src/lib.rs index 16124ffe99..a4fd098b56 100644 --- a/docs/book_examples/src/lib.rs +++ b/docs/book_examples/src/lib.rs @@ -2,6 +2,7 @@ #![allow(dead_code, unused_variables)] +mod custom_widgets_md; mod data_md; mod env_md; mod lens_md; diff --git a/docs/src/custom_widgets.md b/docs/src/custom_widgets.md index 21e4f93e8d..b5db761a25 100644 --- a/docs/src/custom_widgets.md +++ b/docs/src/custom_widgets.md @@ -1,10 +1,88 @@ # Create custom widgets - - controller, painter - - how to do layout - - container widgets - - widgetpod & architecture - - commands and widgetid - - focus / active / hot - - request paint & request layout - - changing widgets at runtime +The `Widget` trait is the heart of druid, and in any serious application you +will eventually need to create and use custom `Widget`s. + +## `Painter` and `Controller` + +There are two helper widgets in druid that let you customize widget behaviour +without needing to implement the full widget trait: [`Painter`] and +[`Controller`]. + +### Painter + +The [`Painter`] widget lets you draw arbitrary custom content, but cannot +respond to events or otherwise contain update logic. Its general use is to +either provide a custom background to some other widget, or to implement +something like an icon or another graphical element that will be contained in +some other widget. + +For instance, if we had some color data and we wanted to display it as a swatch +with rounded corners, we could use a `Painter`: + +```rust,noplaypen +{{#include ../book_examples/src/custom_widgets_md.rs:color_swatch}} +``` + +`Painter` uses all the space that is available to it; if you want to give it a +set size, you must pass it explicit contraints, such as by wrapping it in a +[`SizedBox`]: + +```rust,noplaypen +{{#include ../book_examples/src/custom_widgets_md.rs:sized_swatch}} +``` + +One other useful thing about `Painter` is that it can be used as the background +of a [`Container`] widget. If we wanted to have a label that used our swatch +as a background, we could do (using the [`background`] method on [`WidgetExt`]): + +```rust,noplaypen +{{#include ../book_examples/src/custom_widgets_md.rs:background_label}} +``` + +### Controller + +The [`Controller`] trait is sort of the inverse of `Painter`; it is a way to +make widgets that handle events, but don't do any layout or drawing. The idea +here is that you can use some `Controller` type to customize the behaviour of +some set of children. + +The [`Controller`] trait has `event`, `update`, and `lifecycle` methods, just +like [`Widget`]; it does not have `paint` or `layout` methods. Also unlike +[`Widget`], all of its methods are optional; you can override only the method +that you need. + +There's one other difference to the `Controller` methods; it is explicitly +passed a mutable reference to its child in each method, so that it can modify it +or forward events as needed. + +As an arbitrary example, here is how you might use a `Controller` to change the +behaviour of the `Backspace` key for a textbox, so that every other time it is +pressed it doesn't do anything. (This is a bad example, feel free to write a +better one!): + +```rust,noplaypen +{{#include ../book_examples/src/custom_widgets_md.rs:annoying_textbox}} +``` + +## todo + +v controller, painter +- how to do layout + - how constraints work + - child widget, set_layout_rect + - paint bounds +- container widgets +- widgetpod & architecture +- commands and widgetid +- focus / active / hot +- request paint & request layout +- changing widgets at runtime + +[`Controller`]: https://docs.rs/druid/0.5.0/druid/widget/trait.Controller.html +[`Widget`]: ./widget.md +[`Painter`]: https://docs.rs/druid/0.5.0/druid/widget/struct.Painter.html +[`SizedBox`]: https://docs.rs/druid/0.5.0/druid/widget/struct.SizedBox.html +[`Container`]: https://docs.rs/druid/0.5.0/druid/widget/struct.Container.html +[`WidgetExt`]: https://docs.rs/druid/0.5.0/druid/trait.WidgetExt.html +[`background`]: https://docs.rs/druid/0.5.0/druid/trait.WidgetExt.html#background From 92324059b18d535fa41f0f28b267bc8726e32758 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Tue, 14 Apr 2020 11:50:51 -0400 Subject: [PATCH 2/3] Improve text and controller example --- docs/book_examples/src/custom_widgets_md.rs | 45 +++++++++++++++------ docs/src/custom_widgets.md | 11 ++--- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/docs/book_examples/src/custom_widgets_md.rs b/docs/book_examples/src/custom_widgets_md.rs index e6b21044dd..e81a8892a2 100644 --- a/docs/book_examples/src/custom_widgets_md.rs +++ b/docs/book_examples/src/custom_widgets_md.rs @@ -1,5 +1,9 @@ use druid::widget::{Controller, Label, Painter, SizedBox, TextBox}; -use druid::{Color, Env, Event, EventCtx, KeyCode, PaintCtx, RenderContext, Widget, WidgetExt}; +use druid::{ + Color, Env, Event, EventCtx, KeyCode, PaintCtx, RenderContext, Selector, TimerToken, Widget, + WidgetExt, +}; +use std::time::{Duration, Instant}; const CORNER_RADIUS: f64 = 4.0; const STROKE_WIDTH: f64 = 2.0; @@ -32,12 +36,25 @@ fn background_label() -> impl Widget { // ANCHOR_END: background_label // ANCHOR: annoying_textbox -#[derive(Default)] -struct AnnoyingController { - suppress_next: bool, +const ACTION: Selector = Selector::new("hello.textbox-action"); +const DELAY: Duration = Duration::from_millis(300); + +struct TextBoxActionController { + timer: Option, } -impl Controller for AnnoyingController { +impl TextBoxActionController { + pub fn new() -> Self { + TextBoxActionController { timer: None } + } + + // Fire ACTION after 300 ms + fn deadline() -> Instant { + Instant::now() + DELAY + } +} + +impl Controller for TextBoxActionController { fn event( &mut self, child: &mut TextBox, @@ -46,15 +63,19 @@ impl Controller for AnnoyingController { data: &mut String, env: &Env, ) { - if matches!(event, Event::KeyDown(k) if k.key_code == KeyCode::Backspace) { - self.suppress_next = !self.suppress_next; - if self.suppress_next { - return; + match event { + Event::KeyDown(k) if k.key_code == KeyCode::Return => { + ctx.submit_command(ACTION, None); + } + Event::KeyUp(k) if k.key_code != KeyCode::Return => { + self.timer = Some(ctx.request_timer(Self::deadline())); + child.event(ctx, event, data, env); } + Event::Timer(token) if Some(*token) == self.timer => { + ctx.submit_command(ACTION, None); + } + _ => child.event(ctx, event, data, env), } - - // if we want our child to receive this event, we must send it explicitly. - child.event(ctx, event, data, env); } } // ANCHOR_END: annoying_textbox diff --git a/docs/src/custom_widgets.md b/docs/src/custom_widgets.md index b5db761a25..5dad56caa2 100644 --- a/docs/src/custom_widgets.md +++ b/docs/src/custom_widgets.md @@ -34,12 +34,15 @@ set size, you must pass it explicit contraints, such as by wrapping it in a One other useful thing about `Painter` is that it can be used as the background of a [`Container`] widget. If we wanted to have a label that used our swatch -as a background, we could do (using the [`background`] method on [`WidgetExt`]): +as a background, we could do: ```rust,noplaypen {{#include ../book_examples/src/custom_widgets_md.rs:background_label}} ``` +(This uses the [`background`] method on [`WidgetExt`] to embed our label in a +container.) + ### Controller The [`Controller`] trait is sort of the inverse of `Painter`; it is a way to @@ -56,10 +59,8 @@ There's one other difference to the `Controller` methods; it is explicitly passed a mutable reference to its child in each method, so that it can modify it or forward events as needed. -As an arbitrary example, here is how you might use a `Controller` to change the -behaviour of the `Backspace` key for a textbox, so that every other time it is -pressed it doesn't do anything. (This is a bad example, feel free to write a -better one!): +As an arbitrary example, here is how you might use a `Controller` to make a +textbox fire some action (say doing a search) 300ms after the last keypress: ```rust,noplaypen {{#include ../book_examples/src/custom_widgets_md.rs:annoying_textbox}} From ce16d0ee9fde9808addc454ad544dd27b71d0414 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Tue, 14 Apr 2020 12:10:17 -0400 Subject: [PATCH 3/3] Apply suggestions from code review Co-Authored-By: Leopold Luley --- docs/book_examples/src/custom_widgets_md.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/book_examples/src/custom_widgets_md.rs b/docs/book_examples/src/custom_widgets_md.rs index e81a8892a2..021574a49d 100644 --- a/docs/book_examples/src/custom_widgets_md.rs +++ b/docs/book_examples/src/custom_widgets_md.rs @@ -47,11 +47,6 @@ impl TextBoxActionController { pub fn new() -> Self { TextBoxActionController { timer: None } } - - // Fire ACTION after 300 ms - fn deadline() -> Instant { - Instant::now() + DELAY - } } impl Controller for TextBoxActionController { @@ -68,7 +63,7 @@ impl Controller for TextBoxActionController { ctx.submit_command(ACTION, None); } Event::KeyUp(k) if k.key_code != KeyCode::Return => { - self.timer = Some(ctx.request_timer(Self::deadline())); + self.timer = Some(ctx.request_timer(Instant::now() + DELAY)); child.event(ctx, event, data, env); } Event::Timer(token) if Some(*token) == self.timer => {