druid 0.8.2

Data-oriented Rust UI design toolkit.
Documentation
// Copyright 2019 The Druid Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! A label widget.

use std::ops::{Deref, DerefMut};
use std::sync::Arc;

use druid_shell::Cursor;

use crate::debug_state::DebugState;
use crate::kurbo::Vec2;
use crate::text::TextStorage;
use crate::widget::prelude::*;
use crate::widget::Axis;
use crate::{
    ArcStr, Color, Data, FontDescriptor, KeyOrValue, LocalizedString, Point, TextAlignment,
    TextLayout,
};
use tracing::{instrument, trace, warn};

// added padding between the edges of the widget and the text.
const LABEL_X_PADDING: f64 = 2.0;

/// A label that displays static or dynamic text.
///
/// This type manages an inner [`RawLabel`], updating its text based on the
/// current [`Data`] and [`Env`] as required.
///
/// 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`],
/// and also has methods for setting the default font, font-size, text color,
/// and other attributes.
///
/// In addition to being a [`Widget`], `Label` is also regularly used as a
/// component in other widgets that wish to display text; to facilitate this
/// it has a [`draw_at`] method that allows the caller to easily draw the label's
/// text at the desired position on screen.
///
/// # Examples
///
/// Make a label to say something **very** important:
///
/// ```
/// # use druid::widget::{Label, SizedBox};
/// # use druid::*;
///
/// let font = FontDescriptor::new(FontFamily::SYSTEM_UI)
///     .with_weight(FontWeight::BOLD)
///     .with_size(48.0);
///
/// let important_label = Label::new("WATCH OUT!")
///     .with_font(font)
///     .with_text_color(Color::rgb(1.0, 0.2, 0.2));
/// # // our data type T isn't known; this is just a trick for the compiler
/// # // to keep our example clean
/// # let _ = SizedBox::<()>::new(important_label);
/// ```
///
/// [`draw_at`]: Label::draw_at
pub struct Label<T> {
    label: RawLabel<ArcStr>,
    current_text: ArcStr,
    text: LabelText<T>,
    // for debugging, 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 implement [`TextStorage`]; to handle static, dynamic, or
/// localized text, use [`Label`].
pub struct RawLabel<T> {
    layout: TextLayout<T>,
    line_break_mode: LineBreaking,

    disabled: bool,
    default_text_color: KeyOrValue<Color>,
}

/// Options for handling lines that are too wide for the label.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Data)]
pub enum LineBreaking {
    /// Lines are broken at word boundaries.
    WordWrap,
    /// Lines are truncated to the width of the label.
    Clip,
    /// Lines overflow the label.
    Overflow,
}

/// 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) -> impl Into<ArcStr>`, where
/// `T` is the `Data` at this point in the tree.
#[derive(Clone)]
pub enum LabelText<T> {
    /// Localized string that will be resolved through `Env`.
    Localized(LocalizedString<T>),
    /// 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<T>),
}

/// Text that is computed dynamically.
#[derive(Clone)]
pub struct Dynamic<T> {
    f: Arc<dyn Fn(&T, &Env) -> ArcStr>,
    resolved: ArcStr,
}

/// Static text.
#[derive(Debug, Clone)]
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,
}

impl<T: TextStorage> RawLabel<T> {
    /// Create a new `RawLabel`.
    pub fn new() -> Self {
        Self {
            layout: TextLayout::new(),
            line_break_mode: LineBreaking::Overflow,
            disabled: false,
            default_text_color: crate::theme::TEXT_COLOR.into(),
        }
    }

    /// Builder-style method for setting the text color.
    ///
    /// The argument can be either a `Color` or a [`Key<Color>`].
    ///
    /// [`Key<Color>`]: crate::Key
    pub fn with_text_color(mut self, color: impl Into<KeyOrValue<Color>>) -> Self {
        self.set_text_color(color);
        self
    }

    /// Builder-style method for setting the text size.
    ///
    /// The argument can be either an `f64` or a [`Key<f64>`].
    ///
    /// [`Key<f64>`]: crate::Key
    pub fn with_text_size(mut self, size: impl Into<KeyOrValue<f64>>) -> Self {
        self.set_text_size(size);
        self
    }

    /// Builder-style method for setting the font.
    ///
    /// The argument can be a [`FontDescriptor`] or a [`Key<FontDescriptor>`]
    /// that refers to a font defined in the [`Env`].
    ///
    /// [`Key<FontDescriptor>`]: crate::Key
    pub fn with_font(mut self, font: impl Into<KeyOrValue<FontDescriptor>>) -> Self {
        self.set_font(font);
        self
    }

    /// Builder-style method to set the [`LineBreaking`] behaviour.
    pub fn with_line_break_mode(mut self, mode: LineBreaking) -> Self {
        self.set_line_break_mode(mode);
        self
    }

    /// Builder-style method to set the [`TextAlignment`].
    pub fn with_text_alignment(mut self, alignment: TextAlignment) -> Self {
        self.set_text_alignment(alignment);
        self
    }

    /// Set the text color.
    ///
    /// The argument can be either a `Color` or a [`Key<Color>`].
    ///
    /// If you change this property, you are responsible for calling
    /// [`request_layout`] to ensure the label is updated.
    ///
    /// [`request_layout`]: EventCtx::request_layout
    /// [`Key<Color>`]: crate::Key
    pub fn set_text_color(&mut self, color: impl Into<KeyOrValue<Color>>) {
        let color = color.into();
        if !self.disabled {
            self.layout.set_text_color(color.clone());
        }
        self.default_text_color = color;
    }

    /// Set the text size.
    ///
    /// The argument can be either an `f64` or a [`Key<f64>`].
    ///
    /// If you change this property, you are responsible for calling
    /// [`request_layout`] to ensure the label is updated.
    ///
    /// [`request_layout`]: EventCtx::request_layout
    /// [`Key<f64>`]: crate::Key
    pub fn set_text_size(&mut self, size: impl Into<KeyOrValue<f64>>) {
        self.layout.set_text_size(size);
    }

    /// Set the font.
    ///
    /// The argument can be a [`FontDescriptor`] or a [`Key<FontDescriptor>`]
    /// that refers to a font defined in the [`Env`].
    ///
    /// If you change this property, you are responsible for calling
    /// [`request_layout`] to ensure the label is updated.
    ///
    /// [`request_layout`]: EventCtx::request_layout
    /// [`Key<FontDescriptor>`]: crate::Key
    pub fn set_font(&mut self, font: impl Into<KeyOrValue<FontDescriptor>>) {
        self.layout.set_font(font);
    }

    /// Set the [`LineBreaking`] behaviour.
    ///
    /// If you change this property, you are responsible for calling
    /// [`request_layout`] to ensure the label is updated.
    ///
    /// [`request_layout`]: EventCtx::request_layout
    pub fn set_line_break_mode(&mut self, mode: LineBreaking) {
        self.line_break_mode = mode;
    }

    /// Set the [`TextAlignment`] for this layout.
    pub fn set_text_alignment(&mut self, alignment: TextAlignment) {
        self.layout.set_text_alignment(alignment);
    }

    /// 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<Point>) {
        self.layout.draw(ctx, origin)
    }

    /// Return the offset of the first baseline relative to the bottom of the widget.
    pub fn baseline_offset(&self) -> f64 {
        let text_metrics = self.layout.layout_metrics();
        text_metrics.size.height - text_metrics.first_baseline
    }
}

impl<T: TextStorage> Label<T> {
    /// Create a new [`RawLabel`].
    ///
    /// This can display text `Data` directly.
    pub fn raw() -> RawLabel<T> {
        RawLabel::new()
    }
}

impl<T: Data> Label<T> {
    /// Construct a new `Label` widget.
    ///
    /// ```
    /// use druid::LocalizedString;
    /// use druid::widget::Label;
    ///
    /// // Construct a new Label using static string.
    /// let _: Label<u32> = 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<u32> = Label::new(text);
    ///
    /// // Construct a new dynamic Label. Text will be updated when data changes.
    /// let _: Label<u32> = Label::new(|data: &u32, _env: &_| format!("Hello world: {}", data));
    /// ```
    pub fn new(text: impl Into<LabelText<T>>) -> Self {
        let text = text.into();
        let current_text = text.display_text();
        Self {
            text,
            current_text,
            label: RawLabel::new(),
            text_should_be_updated: 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<u32> = Label::new(|data: &u32, _: &Env| format!("total is {}", data));
    /// let label2: Label<u32> = Label::dynamic(|data, _| format!("total is {}", data));
    /// ```
    ///
    /// [`new`]: Label::new
    pub fn dynamic(text: impl Fn(&T, &Env) -> String + 'static) -> Self {
        let text: LabelText<T> = text.into();
        Label::new(text)
    }

    /// Return the current value of the label's text.
    pub fn text(&self) -> ArcStr {
        self.text.display_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`]: Widget::update
    /// [`request_update`]: EventCtx::request_update
    pub fn set_text(&mut self, text: impl Into<LabelText<T>>) {
        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<Color>`].
    ///
    /// [`Key<Color>`]: crate::Key
    pub fn with_text_color(mut self, color: impl Into<KeyOrValue<Color>>) -> 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<f64>`].
    ///
    /// [`Key<f64>`]: crate::Key
    pub fn with_text_size(mut self, size: impl Into<KeyOrValue<f64>>) -> Self {
        self.label.set_text_size(size);
        self
    }

    /// Builder-style method for setting the font.
    ///
    /// The argument can be a [`FontDescriptor`] or a [`Key<FontDescriptor>`]
    /// that refers to a font defined in the [`Env`].
    ///
    /// [`Key<FontDescriptor>`]: crate::Key
    pub fn with_font(mut self, font: impl Into<KeyOrValue<FontDescriptor>>) -> Self {
        self.label.set_font(font);
        self
    }

    /// Builder-style method to set the [`LineBreaking`] behaviour.
    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`].
    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<Point>) {
        self.label.draw_at(ctx, origin)
    }
}

impl Static {
    fn new(s: ArcStr) -> Self {
        Static {
            string: s,
            resolved: false,
        }
    }

    fn resolve(&mut self) -> bool {
        let is_first_call = !self.resolved;
        self.resolved = true;
        is_first_call
    }
}

impl<T> Dynamic<T> {
    fn resolve(&mut self, data: &T, env: &Env) -> bool {
        let new = (self.f)(data, env);
        let changed = new != self.resolved;
        self.resolved = new;
        changed
    }
}

impl<T: Data> LabelText<T> {
    /// Call callback with the text that should be displayed.
    pub fn with_display_text<V>(&self, mut cb: impl FnMut(&str) -> V) -> V {
        match self {
            LabelText::Static(s) => cb(&s.string),
            LabelText::Localized(s) => cb(&s.localized_str()),
            LabelText::Dynamic(s) => cb(&s.resolved),
        }
    }

    /// Return the current resolved text.
    pub fn display_text(&self) -> ArcStr {
        match self {
            LabelText::Static(s) => s.string.clone(),
            LabelText::Localized(s) => s.localized_str(),
            LabelText::Dynamic(s) => s.resolved.clone(),
        }
    }

    /// Update the localization, if necessary.
    /// This ensures that localized strings are up to date.
    ///
    /// Returns `true` if the string has changed.
    pub fn resolve(&mut self, data: &T, env: &Env) -> bool {
        match self {
            LabelText::Static(s) => s.resolve(),
            LabelText::Localized(s) => s.resolve(data, env),
            LabelText::Dynamic(s) => s.resolve(data, env),
        }
    }
}

impl<T: Data> Widget<T> for Label<T> {
    #[instrument(name = "Label", level = "trace", skip(self, _ctx, _event, _data, _env))]
    fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut T, _env: &Env) {}

    #[instrument(name = "Label", level = "trace", skip(self, ctx, event, data, 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);
    }

    #[instrument(name = "Label", level = "trace", skip(self, ctx, _old_data, data, 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);
        }
    }

    #[instrument(name = "Label", level = "trace", skip(self, ctx, bc, _data, env))]
    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &T, env: &Env) -> Size {
        self.label.layout(ctx, bc, &self.current_text, env)
    }

    #[instrument(name = "Label", level = "trace", skip(self, ctx, _data, env))]
    fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, env: &Env) {
        if self.text_should_be_updated {
            tracing::warn!("Label text changed without call to update. See LabelAdapter::set_text for information.");
        }
        self.label.paint(ctx, &self.current_text, env)
    }

    fn debug_state(&self, _data: &T) -> DebugState {
        DebugState {
            display_name: self.short_type_name().to_string(),
            main_value: self.current_text.to_string(),
            ..Default::default()
        }
    }

    fn compute_max_intrinsic(
        &mut self,
        axis: Axis,
        ctx: &mut LayoutCtx,
        bc: &BoxConstraints,
        _data: &T,
        env: &Env,
    ) -> f64 {
        self.label
            .compute_max_intrinsic(axis, ctx, bc, &self.current_text, env)
    }
}

impl<T: TextStorage> Widget<T> for RawLabel<T> {
    #[instrument(
        name = "RawLabel",
        level = "trace",
        skip(self, ctx, event, _data, _env)
    )]
    fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut T, _env: &Env) {
        match event {
            Event::MouseUp(event) => {
                // Account for the padding
                let pos = event.pos - Vec2::new(LABEL_X_PADDING, 0.0);
                if let Some(link) = self.layout.link_for_pos(pos) {
                    ctx.submit_command(link.command.clone());
                }
            }
            Event::MouseMove(event) => {
                // Account for the padding
                let pos = event.pos - Vec2::new(LABEL_X_PADDING, 0.0);

                if self.layout.link_for_pos(pos).is_some() {
                    ctx.set_cursor(&Cursor::Pointer);
                } else {
                    ctx.clear_cursor();
                }
            }
            _ => {}
        }
    }

    #[instrument(name = "RawLabel", level = "trace", skip(self, ctx, event, data, _env))]
    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, _env: &Env) {
        match event {
            LifeCycle::WidgetAdded => {
                self.layout.set_text(data.to_owned());
            }
            LifeCycle::DisabledChanged(disabled) => {
                let color = if *disabled {
                    KeyOrValue::Key(crate::theme::DISABLED_TEXT_COLOR)
                } else {
                    self.default_text_color.clone()
                };
                self.layout.set_text_color(color);
                ctx.request_layout();
            }
            _ => {}
        }
    }

    #[instrument(
        name = "RawLabel",
        level = "trace",
        skip(self, ctx, old_data, data, _env)
    )]
    fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, _env: &Env) {
        if !old_data.same(data) {
            self.layout.set_text(data.clone());
            ctx.request_layout();
        }
        if self.layout.needs_rebuild_after_update(ctx) {
            ctx.request_layout();
        }
    }

    #[instrument(name = "RawLabel", level = "trace", skip(self, ctx, bc, _data, env))]
    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &T, env: &Env) -> Size {
        bc.debug_check("Label");

        let width = match self.line_break_mode {
            LineBreaking::WordWrap => bc.max().width - LABEL_X_PADDING * 2.0,
            _ => f64::INFINITY,
        };

        self.layout.set_wrap_width(width);
        self.layout.rebuild_if_needed(ctx.text(), env);

        let text_metrics = self.layout.layout_metrics();
        ctx.set_baseline_offset(text_metrics.size.height - text_metrics.first_baseline);
        let size = bc.constrain(Size::new(
            text_metrics.size.width + 2. * LABEL_X_PADDING,
            text_metrics.size.height,
        ));
        trace!("Computed size: {}", size);
        size
    }

    #[instrument(name = "RawLabel", level = "trace", skip(self, ctx, _data, _env))]
    fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, _env: &Env) {
        let origin = Point::new(LABEL_X_PADDING, 0.0);
        let label_size = ctx.size();

        if self.line_break_mode == LineBreaking::Clip {
            ctx.clip(label_size.to_rect());
        }
        self.draw_at(ctx, origin)
    }

    fn compute_max_intrinsic(
        &mut self,
        axis: Axis,
        ctx: &mut LayoutCtx,
        bc: &BoxConstraints,
        data: &T,
        env: &Env,
    ) -> f64 {
        match axis {
            Axis::Horizontal => {
                match self.line_break_mode {
                    LineBreaking::WordWrap => {
                        // Height is irrelevant for labels. So max preferred/intrinsic width of a label is the size
                        // it'd take without any word wrapping.
                        self.line_break_mode = LineBreaking::Clip;
                        let s = self.layout(ctx, bc, data, env);
                        self.line_break_mode = LineBreaking::WordWrap;
                        s.width
                    }
                    _ => self.layout(ctx, bc, data, env).width,
                }
            }
            Axis::Vertical => {
                warn!("Max intrinsic height of a label is not implemented.");
                0.
            }
        }
    }
}

impl<T: TextStorage> Default for RawLabel<T> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T> Deref for Label<T> {
    type Target = RawLabel<ArcStr>;
    fn deref(&self) -> &Self::Target {
        &self.label
    }
}

impl<T> DerefMut for Label<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.label
    }
}
impl<T> From<String> for LabelText<T> {
    fn from(src: String) -> LabelText<T> {
        LabelText::Static(Static::new(src.into()))
    }
}

impl<T> From<&str> for LabelText<T> {
    fn from(src: &str) -> LabelText<T> {
        LabelText::Static(Static::new(src.into()))
    }
}

impl<T> From<ArcStr> for LabelText<T> {
    fn from(string: ArcStr) -> LabelText<T> {
        LabelText::Static(Static::new(string))
    }
}

impl<T> From<LocalizedString<T>> for LabelText<T> {
    fn from(src: LocalizedString<T>) -> LabelText<T> {
        LabelText::Localized(src)
    }
}

impl<T, S, F> From<F> for LabelText<T>
where
    S: Into<Arc<str>>,
    F: Fn(&T, &Env) -> S + 'static,
{
    fn from(src: F) -> LabelText<T> {
        let f = Arc::new(move |state: &T, env: &Env| src(state, env).into());
        LabelText::Dynamic(Dynamic {
            f,
            resolved: ArcStr::from(""),
        })
    }
}