From 6df436a5864c5041bdb8d463802912e806fd2e78 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Sat, 19 Sep 2020 11:33:24 -0400 Subject: [PATCH] Separate Label and RawLabel This makes label work more like other widgets; the RawLabel widget is a thing that has a text data type (currently ArcStr) and displays this text. The existing Label widget is rewritten as a wrapper around RawLabel; it is basically a sort of specialized, env-aware LensWrap. --- CHANGELOG.md | 2 + druid/src/widget/label.rs | 382 ++++++++++++++++++++++++++------------ druid/src/widget/mod.rs | 2 +- 3 files changed, 270 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5209bdf1f0..f0d8a30050 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ You can find its changes [documented below](#060---2020-06-01). - `WindowLevel` to control system window Z order, with Mac and GTK implementations ([#1231] by [@rjwittams]) - WIDGET_PADDING items added to theme and `Flex::with_default_spacer`/`Flex::add_default_spacer` ([#1220] by [@cmyr]) - CONFIGURE_WINDOW command to allow reconfiguration of an existing window. ([#1235] by [@rjwittams]) +- `RawLabel` widget displays text `Data`. ([#1252] by [@cmyr]) ### Changed @@ -467,6 +468,7 @@ Last release without a changelog :( [#1241]: https://github.com/linebender/druid/pull/1241 [#1245]: https://github.com/linebender/druid/pull/1245 [#1251]: https://github.com/linebender/druid/pull/1251 +[#1252]: https://github.com/linebender/druid/pull/1252 [Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master [0.6.0]: https://github.com/linebender/druid/compare/v0.5.0...v0.6.0 diff --git a/druid/src/widget/label.rs b/druid/src/widget/label.rs index 4f6e2799a2..b35f1bdc37 100644 --- a/druid/src/widget/label.rs +++ b/druid/src/widget/label.rs @@ -14,54 +14,25 @@ //! A label widget. -use crate::piet::{Color, PietText}; +use std::ops::{Deref, DerefMut}; + use crate::widget::prelude::*; use crate::{ - ArcStr, BoxConstraints, Data, FontDescriptor, KeyOrValue, LocalizedString, Point, Size, + ArcStr, BoxConstraints, Color, Data, FontDescriptor, KeyOrValue, LocalizedString, Point, Size, TextAlignment, TextLayout, }; // added padding between the edges of the widget and the text. const LABEL_X_PADDING: f64 = 2.0; -/// The text for the label. +/// A label that displays static or dynamic text. /// -/// This can be one of three things; either a [`ArcStr`], a [`LocalizedString`], -/// or a closure with the signature, `Fn(&T, &Env) -> String`, where `T` is -/// the [`Data`] at this point in the tree. +/// This type manages an inner [`RawLabel`], updating its text based on the +/// current [`Data`] and [`Env`] as required. /// -/// [`ArcStr`]: ../type.ArcStr.html -/// [`LocalizedString`]: ../struct.LocalizedString.html -/// [`Data`]: ../trait.Data.html -pub enum LabelText { - /// Localized string that will be resolved through `Env`. - Localized(LocalizedString), - /// Static text. - Static(Static), - /// The provided closure is called on update, and its return - /// value is used as the text for the label. - Dynamic(Dynamic), -} - -/// Text that is computed dynamically. -pub struct Dynamic { - f: Box String>, - resolved: ArcStr, -} - -/// Static text. -pub struct Static { - /// The text. - string: ArcStr, - /// Whether or not the `resolved` method has been called yet. - /// - /// We want to return `true` from that method when it is first called, - /// so that callers will know to retrieve the text. This matches - /// the behaviour of the other variants. - resolved: bool, -} - -/// A label that draws some text. +/// If your [`Data`] is *already* text, you may use a [`RawLabel`] directly. +/// As a convenience, you can create a [`RawLabel`] with the [`Label::raw`] +/// constructor method. /// /// A label is the easiest way to display text in Druid. A label is instantiated /// with some [`LabelText`] type, such as an [`ArcStr`] or a [`LocalizedString`], @@ -94,17 +65,32 @@ pub struct Static { /// ``` /// /// [`ArcStr`]: ../type.ArcStr.html +/// [`Data`]: ../trait.Data.html +/// [`Env`]: ../struct.Env.html +/// [`RawLabel`]: struct.RawLabel.html +/// [`Label::raw`]: #method.raw /// [`LabelText`]: struct.LabelText.html /// [`LocalizedString`]: ../struct.LocalizedString.html /// [`draw_at`]: #method.draw_at /// [`Widget`]: ../trait.Widget.html pub struct Label { + label: RawLabel, + current_text: ArcStr, text: LabelText, + // for debuging, we track if the user modifies the text and we don't get + // an update call, which might cause us to display stale text. + text_should_be_updated: bool, +} + +/// A widget that displays text data. +/// +/// This requires the `Data` to be `ArcStr`; to handle static, dynamic, or +/// localized text, use [`Label`]. +/// +/// [`Label`]: struct.Label.html +pub struct RawLabel { layout: TextLayout, line_break_mode: LineBreaking, - // if our text is manually changed we need to rebuild the layout - // before using it again. - needs_rebuild: bool, } /// Options for handling lines that are too wide for the label. @@ -118,59 +104,53 @@ pub enum LineBreaking { Overflow, } -impl Label { - /// Construct a new `Label` widget. - /// - /// ``` - /// use druid::LocalizedString; - /// use druid::widget::Label; - /// - /// // Construct a new Label using static string. - /// let _: Label = Label::new("Hello world"); - /// - /// // Construct a new Label using localized string. - /// let text = LocalizedString::new("hello-counter").with_arg("count", |data: &u32, _env| (*data).into()); - /// let _: Label = Label::new(text); +/// The text for a [`Label`]. +/// +/// This can be one of three things; either an [`ArcStr`], a [`LocalizedString`], +/// or a closure with the signature, `Fn(&T, &Env) -> String`, where `T` is +/// the `Data` at this point in the tree. +/// +/// [`ArcStr`]: ../type.ArcStr.html +/// [`LocalizedString`]: ../struct.LocalizedString.html +/// [`Label`]: struct.Label.html +pub enum LabelText { + /// Localized string that will be resolved through `Env`. + Localized(LocalizedString), + /// Static text. + Static(Static), + /// The provided closure is called on update, and its return + /// value is used as the text for the label. + Dynamic(Dynamic), +} + +/// Text that is computed dynamically. +pub struct Dynamic { + f: Box String>, + resolved: ArcStr, +} + +/// Static text. +pub struct Static { + /// The text. + string: ArcStr, + /// Whether or not the `resolved` method has been called yet. /// - /// // Construct a new dynamic Label. Text will be updated when data changes. - /// let _: Label = Label::new(|data: &u32, _env: &_| format!("Hello world: {}", data)); - /// ``` - pub fn new(text: impl Into>) -> Self { - let text = text.into(); - let layout = TextLayout::new(text.display_text()); + /// We want to return `true` from that method when it is first called, + /// so that callers will know to retrieve the text. This matches + /// the behaviour of the other variants. + resolved: bool, +} + +impl RawLabel { + /// Create a new `RawLabel`. + pub fn new() -> Self { + let layout = TextLayout::new(""); Self { - text, layout, line_break_mode: LineBreaking::Overflow, - needs_rebuild: true, } } - /// Construct a new dynamic label. - /// - /// The contents of this label are generated from the data using a closure. - /// - /// This is provided as a convenience; a closure can also be passed to [`new`], - /// but due to limitations of the implementation of that method, the types in - /// the closure need to be annotated, which is not true for this method. - /// - /// # Examples - /// - /// The following are equivalent. - /// - /// ``` - /// use druid::Env; - /// use druid::widget::Label; - /// let label1: Label = Label::new(|data: &u32, _: &Env| format!("total is {}", data)); - /// let label2: Label = Label::dynamic(|data, _| format!("total is {}", data)); - /// ``` - /// - /// [`new`]: #method.new - pub fn dynamic(text: impl Fn(&T, &Env) -> String + 'static) -> Self { - let text: LabelText = text.into(); - Label::new(text) - } - /// Builder-style method for setting the text color. /// /// The argument can be either a `Color` or a [`Key`]. @@ -220,17 +200,6 @@ impl Label { self } - /// Set the label's text. - /// - /// If you change this property, you are responsible for calling - /// [`request_layout`] to ensure the label is updated. - /// - /// [`request_layout`]: ../struct.EventCtx.html#method.request_layout - pub fn set_text(&mut self, text: impl Into>) { - self.text = text.into(); - self.needs_rebuild = true; - } - /// Set the text color. /// /// The argument can be either a `Color` or a [`Key`]. @@ -242,7 +211,6 @@ impl Label { /// [`Key`]: ../struct.Key.html pub fn set_text_color(&mut self, color: impl Into>) { self.layout.set_text_color(color); - self.needs_rebuild = true; } /// Set the text size. @@ -256,7 +224,6 @@ impl Label { /// [`Key`]: ../struct.Key.html pub fn set_text_size(&mut self, size: impl Into>) { self.layout.set_text_size(size); - self.needs_rebuild = true; } /// Set the font. @@ -273,7 +240,6 @@ impl Label { /// [`Key`]: ../struct.Key.html pub fn set_font(&mut self, font: impl Into>) { self.layout.set_font(font); - self.needs_rebuild = true; } /// Set the [`LineBreaking`] behaviour. @@ -285,7 +251,6 @@ impl Label { /// [`LineBreaking`]: enum.LineBreaking.html pub fn set_line_break_mode(&mut self, mode: LineBreaking) { self.line_break_mode = mode; - self.needs_rebuild = true; } /// Set the [`TextAlignment`] for this layout. @@ -293,7 +258,6 @@ impl Label { /// [`TextAlignment`]: enum.TextAlignment.html pub fn set_text_alignment(&mut self, alignment: TextAlignment) { self.layout.set_text_alignment(alignment); - self.needs_rebuild = true; } /// Draw this label's text at the provided `Point`, without internal padding. @@ -304,15 +268,140 @@ impl Label { pub fn draw_at(&self, ctx: &mut PaintCtx, origin: impl Into) { self.layout.draw(ctx, origin) } +} - fn rebuild_if_needed(&mut self, factory: &mut PietText, data: &T, env: &Env) { - if self.needs_rebuild || self.layout.needs_rebuild() { - self.text.resolve(data, env); - self.layout.set_text(self.text.display_text()); - self.layout.rebuild_if_needed(factory, env); - self.needs_rebuild = false; +impl Label { + /// Construct a new `Label` widget. + /// + /// ``` + /// use druid::LocalizedString; + /// use druid::widget::Label; + /// + /// // Construct a new Label using static string. + /// let _: Label = Label::new("Hello world"); + /// + /// // Construct a new Label using localized string. + /// let text = LocalizedString::new("hello-counter").with_arg("count", |data: &u32, _env| (*data).into()); + /// let _: Label = Label::new(text); + /// + /// // Construct a new dynamic Label. Text will be updated when data changes. + /// let _: Label = Label::new(|data: &u32, _env: &_| format!("Hello world: {}", data)); + /// ``` + pub fn new(text: impl Into>) -> Self { + let text = text.into(); + let current_text = text.display_text(); + Self { + text, + current_text, + label: RawLabel::new(), + text_should_be_updated: true, } } + + /// Create a new [`RawLabel`]. + /// + /// This can display text `Data` directly. + pub fn raw() -> RawLabel { + RawLabel::new() + } + + /// Construct a new dynamic label. + /// + /// The contents of this label are generated from the data using a closure. + /// + /// This is provided as a convenience; a closure can also be passed to [`new`], + /// but due to limitations of the implementation of that method, the types in + /// the closure need to be annotated, which is not true for this method. + /// + /// # Examples + /// + /// The following are equivalent. + /// + /// ``` + /// use druid::Env; + /// use druid::widget::Label; + /// let label1: Label = Label::new(|data: &u32, _: &Env| format!("total is {}", data)); + /// let label2: Label = Label::dynamic(|data, _| format!("total is {}", data)); + /// ``` + /// + /// [`new`]: #method.new + pub fn dynamic(text: impl Fn(&T, &Env) -> String + 'static) -> Self { + let text: LabelText = text.into(); + Label::new(text) + } + + /// Set the label's text. + /// + /// # Note + /// + /// If you change this property, at runtime, you **must** ensure that [`update`] + /// is called in order to correctly recompute the text. If you are unsure, + /// call [`request_update`] explicitly. + /// + /// [`update`]: ../trait.Widget.html#tymethod.update + /// [`request_update`]: ../struct.EventCtx.html#method.request_update + pub fn set_text(&mut self, text: impl Into>) { + self.text = text.into(); + self.text_should_be_updated = true; + } + + /// Builder-style method for setting the text color. + /// + /// The argument can be either a `Color` or a [`Key`]. + /// + /// [`Key`]: ../struct.Key.html + pub fn with_text_color(mut self, color: impl Into>) -> Self { + self.label.set_text_color(color); + self + } + + /// Builder-style method for setting the text size. + /// + /// The argument can be either an `f64` or a [`Key`]. + /// + /// [`Key`]: ../struct.Key.html + pub fn with_text_size(mut self, size: impl Into>) -> Self { + self.label.set_text_size(size); + self + } + + /// Builder-style method for setting the font. + /// + /// The argument can be a [`FontDescriptor`] or a [`Key`] + /// that refers to a font defined in the [`Env`]. + /// + /// [`Env`]: ../struct.Env.html + /// [`FontDescriptor`]: ../struct.FontDescriptor.html + /// [`Key`]: ../struct.Key.html + pub fn with_font(mut self, font: impl Into>) -> Self { + self.label.set_font(font); + self + } + + /// Builder-style method to set the [`LineBreaking`] behaviour. + /// + /// [`LineBreaking`]: enum.LineBreaking.html + pub fn with_line_break_mode(mut self, mode: LineBreaking) -> Self { + self.label.set_line_break_mode(mode); + self + } + + /// Builder-style method to set the [`TextAlignment`]. + /// + /// [`TextAlignment`]: enum.TextAlignment.html + pub fn with_text_alignment(mut self, alignment: TextAlignment) -> Self { + self.label.set_text_alignment(alignment); + self + } + + /// Draw this label's text at the provided `Point`, without internal padding. + /// + /// This is a convenience for widgets that want to use Label as a way + /// of managing a dynamic or localized string, but want finer control + /// over where the text is drawn. + pub fn draw_at(&self, ctx: &mut PaintCtx, origin: impl Into) { + self.label.draw_at(ctx, origin) + } } impl Static { @@ -374,16 +463,61 @@ impl LabelText { impl Widget for Label { fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut T, _env: &Env) {} - fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle, _data: &T, _env: &Env) {} + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { + if matches!(event, LifeCycle::WidgetAdded) { + self.text.resolve(data, env); + self.text_should_be_updated = false; + self.label + .lifecycle(ctx, event, &self.text.display_text(), env); + } + } + + fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) { + let data_changed = self.text.resolve(data, env); + self.text_should_be_updated = false; + if data_changed { + let new_text = self.text.display_text(); + self.label.update(ctx, &self.current_text, &new_text, env); + self.current_text = new_text; + } else if ctx.env_changed() { + self.label + .update(ctx, &self.current_text, &self.current_text, env); + } + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &T, env: &Env) -> Size { + self.label.layout(ctx, bc, &self.current_text, env) + } + + fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, env: &Env) { + if self.text_should_be_updated { + log::warn!("Label text changed without call to update. See LabelAdapter::set_text for information."); + } + self.label.paint(ctx, &self.current_text, env) + } +} + +impl Widget for RawLabel { + fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut ArcStr, _env: &Env) {} + fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &ArcStr, _env: &Env) { + if matches!(event, LifeCycle::WidgetAdded) { + self.layout.set_text(data.clone()); + } + } - fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, _env: &Env) { + fn update(&mut self, ctx: &mut UpdateCtx, old_data: &ArcStr, data: &ArcStr, _env: &Env) { if !old_data.same(data) | self.layout.needs_rebuild_after_update(ctx) { - self.needs_rebuild = true; ctx.request_layout(); } } - fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { + fn layout( + &mut self, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + _data: &ArcStr, + env: &Env, + ) -> Size { bc.debug_check("Label"); let width = match self.line_break_mode { @@ -392,14 +526,14 @@ impl Widget for Label { }; self.layout.set_wrap_width(width); - self.rebuild_if_needed(&mut ctx.text(), data, env); + self.layout.rebuild_if_needed(ctx.text(), env); let mut text_size = self.layout.size(); text_size.width += 2. * LABEL_X_PADDING; bc.constrain(text_size) } - fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, _env: &Env) { + fn paint(&mut self, ctx: &mut PaintCtx, _data: &ArcStr, _env: &Env) { let origin = Point::new(LABEL_X_PADDING, 0.0); let label_size = ctx.size(); @@ -410,6 +544,24 @@ impl Widget for Label { } } +impl Default for RawLabel { + fn default() -> Self { + Self::new() + } +} + +impl Deref for Label { + type Target = RawLabel; + fn deref(&self) -> &Self::Target { + &self.label + } +} + +impl DerefMut for Label { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.label + } +} impl From for LabelText { fn from(src: String) -> LabelText { LabelText::Static(Static::new(src.into())) diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 7383fb19f8..96f466d0bd 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -64,7 +64,7 @@ pub use either::Either; pub use env_scope::EnvScope; pub use flex::{CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment}; pub use identity_wrapper::IdentityWrapper; -pub use label::{Label, LabelText, LineBreaking}; +pub use label::{Label, LabelText, LineBreaking, RawLabel}; pub use lens_wrap::LensWrap; pub use list::{List, ListIter}; pub use padding::Padding;