druid/widget/
label.rs

1// Copyright 2019 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! A label widget.
16
17use std::ops::{Deref, DerefMut};
18use std::sync::Arc;
19
20use druid_shell::Cursor;
21
22use crate::debug_state::DebugState;
23use crate::kurbo::Vec2;
24use crate::text::TextStorage;
25use crate::widget::prelude::*;
26use crate::widget::Axis;
27use crate::{
28    ArcStr, Color, Data, FontDescriptor, KeyOrValue, LocalizedString, Point, TextAlignment,
29    TextLayout,
30};
31use tracing::{instrument, trace, warn};
32
33// added padding between the edges of the widget and the text.
34const LABEL_X_PADDING: f64 = 2.0;
35
36/// A label that displays static or dynamic text.
37///
38/// This type manages an inner [`RawLabel`], updating its text based on the
39/// current [`Data`] and [`Env`] as required.
40///
41/// If your [`Data`] is *already* text, you may use a [`RawLabel`] directly.
42/// As a convenience, you can create a [`RawLabel`] with the [`Label::raw`]
43/// constructor method.
44///
45/// A label is the easiest way to display text in Druid. A label is instantiated
46/// with some [`LabelText`] type, such as an [`ArcStr`] or a [`LocalizedString`],
47/// and also has methods for setting the default font, font-size, text color,
48/// and other attributes.
49///
50/// In addition to being a [`Widget`], `Label` is also regularly used as a
51/// component in other widgets that wish to display text; to facilitate this
52/// it has a [`draw_at`] method that allows the caller to easily draw the label's
53/// text at the desired position on screen.
54///
55/// # Examples
56///
57/// Make a label to say something **very** important:
58///
59/// ```
60/// # use druid::widget::{Label, SizedBox};
61/// # use druid::*;
62///
63/// let font = FontDescriptor::new(FontFamily::SYSTEM_UI)
64///     .with_weight(FontWeight::BOLD)
65///     .with_size(48.0);
66///
67/// let important_label = Label::new("WATCH OUT!")
68///     .with_font(font)
69///     .with_text_color(Color::rgb(1.0, 0.2, 0.2));
70/// # // our data type T isn't known; this is just a trick for the compiler
71/// # // to keep our example clean
72/// # let _ = SizedBox::<()>::new(important_label);
73/// ```
74///
75/// [`draw_at`]: Label::draw_at
76pub struct Label<T> {
77    label: RawLabel<ArcStr>,
78    current_text: ArcStr,
79    text: LabelText<T>,
80    // for debugging, we track if the user modifies the text and we don't get
81    // an update call, which might cause us to display stale text.
82    text_should_be_updated: bool,
83}
84
85/// A widget that displays text data.
86///
87/// This requires the `Data` to implement [`TextStorage`]; to handle static, dynamic, or
88/// localized text, use [`Label`].
89pub struct RawLabel<T> {
90    layout: TextLayout<T>,
91    line_break_mode: LineBreaking,
92
93    disabled: bool,
94    default_text_color: KeyOrValue<Color>,
95}
96
97/// Options for handling lines that are too wide for the label.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Data)]
99pub enum LineBreaking {
100    /// Lines are broken at word boundaries.
101    WordWrap,
102    /// Lines are truncated to the width of the label.
103    Clip,
104    /// Lines overflow the label.
105    Overflow,
106}
107
108/// The text for a [`Label`].
109///
110/// This can be one of three things; either an [`ArcStr`], a [`LocalizedString`],
111/// or a closure with the signature `Fn(&T, &Env) -> impl Into<ArcStr>`, where
112/// `T` is the `Data` at this point in the tree.
113#[derive(Clone)]
114pub enum LabelText<T> {
115    /// Localized string that will be resolved through `Env`.
116    Localized(LocalizedString<T>),
117    /// Static text.
118    Static(Static),
119    /// The provided closure is called on update, and its return
120    /// value is used as the text for the label.
121    Dynamic(Dynamic<T>),
122}
123
124/// Text that is computed dynamically.
125#[derive(Clone)]
126pub struct Dynamic<T> {
127    f: Arc<dyn Fn(&T, &Env) -> ArcStr>,
128    resolved: ArcStr,
129}
130
131/// Static text.
132#[derive(Debug, Clone)]
133pub struct Static {
134    /// The text.
135    string: ArcStr,
136    /// Whether or not the `resolved` method has been called yet.
137    ///
138    /// We want to return `true` from that method when it is first called,
139    /// so that callers will know to retrieve the text. This matches
140    /// the behaviour of the other variants.
141    resolved: bool,
142}
143
144impl<T: TextStorage> RawLabel<T> {
145    /// Create a new `RawLabel`.
146    pub fn new() -> Self {
147        Self {
148            layout: TextLayout::new(),
149            line_break_mode: LineBreaking::Overflow,
150            disabled: false,
151            default_text_color: crate::theme::TEXT_COLOR.into(),
152        }
153    }
154
155    /// Builder-style method for setting the text color.
156    ///
157    /// The argument can be either a `Color` or a [`Key<Color>`].
158    ///
159    /// [`Key<Color>`]: crate::Key
160    pub fn with_text_color(mut self, color: impl Into<KeyOrValue<Color>>) -> Self {
161        self.set_text_color(color);
162        self
163    }
164
165    /// Builder-style method for setting the text size.
166    ///
167    /// The argument can be either an `f64` or a [`Key<f64>`].
168    ///
169    /// [`Key<f64>`]: crate::Key
170    pub fn with_text_size(mut self, size: impl Into<KeyOrValue<f64>>) -> Self {
171        self.set_text_size(size);
172        self
173    }
174
175    /// Builder-style method for setting the font.
176    ///
177    /// The argument can be a [`FontDescriptor`] or a [`Key<FontDescriptor>`]
178    /// that refers to a font defined in the [`Env`].
179    ///
180    /// [`Key<FontDescriptor>`]: crate::Key
181    pub fn with_font(mut self, font: impl Into<KeyOrValue<FontDescriptor>>) -> Self {
182        self.set_font(font);
183        self
184    }
185
186    /// Builder-style method to set the [`LineBreaking`] behaviour.
187    pub fn with_line_break_mode(mut self, mode: LineBreaking) -> Self {
188        self.set_line_break_mode(mode);
189        self
190    }
191
192    /// Builder-style method to set the [`TextAlignment`].
193    pub fn with_text_alignment(mut self, alignment: TextAlignment) -> Self {
194        self.set_text_alignment(alignment);
195        self
196    }
197
198    /// Set the text color.
199    ///
200    /// The argument can be either a `Color` or a [`Key<Color>`].
201    ///
202    /// If you change this property, you are responsible for calling
203    /// [`request_layout`] to ensure the label is updated.
204    ///
205    /// [`request_layout`]: EventCtx::request_layout
206    /// [`Key<Color>`]: crate::Key
207    pub fn set_text_color(&mut self, color: impl Into<KeyOrValue<Color>>) {
208        let color = color.into();
209        if !self.disabled {
210            self.layout.set_text_color(color.clone());
211        }
212        self.default_text_color = color;
213    }
214
215    /// Set the text size.
216    ///
217    /// The argument can be either an `f64` or a [`Key<f64>`].
218    ///
219    /// If you change this property, you are responsible for calling
220    /// [`request_layout`] to ensure the label is updated.
221    ///
222    /// [`request_layout`]: EventCtx::request_layout
223    /// [`Key<f64>`]: crate::Key
224    pub fn set_text_size(&mut self, size: impl Into<KeyOrValue<f64>>) {
225        self.layout.set_text_size(size);
226    }
227
228    /// Set the font.
229    ///
230    /// The argument can be a [`FontDescriptor`] or a [`Key<FontDescriptor>`]
231    /// that refers to a font defined in the [`Env`].
232    ///
233    /// If you change this property, you are responsible for calling
234    /// [`request_layout`] to ensure the label is updated.
235    ///
236    /// [`request_layout`]: EventCtx::request_layout
237    /// [`Key<FontDescriptor>`]: crate::Key
238    pub fn set_font(&mut self, font: impl Into<KeyOrValue<FontDescriptor>>) {
239        self.layout.set_font(font);
240    }
241
242    /// Set the [`LineBreaking`] behaviour.
243    ///
244    /// If you change this property, you are responsible for calling
245    /// [`request_layout`] to ensure the label is updated.
246    ///
247    /// [`request_layout`]: EventCtx::request_layout
248    pub fn set_line_break_mode(&mut self, mode: LineBreaking) {
249        self.line_break_mode = mode;
250    }
251
252    /// Set the [`TextAlignment`] for this layout.
253    pub fn set_text_alignment(&mut self, alignment: TextAlignment) {
254        self.layout.set_text_alignment(alignment);
255    }
256
257    /// Draw this label's text at the provided `Point`, without internal padding.
258    ///
259    /// This is a convenience for widgets that want to use Label as a way
260    /// of managing a dynamic or localized string, but want finer control
261    /// over where the text is drawn.
262    pub fn draw_at(&self, ctx: &mut PaintCtx, origin: impl Into<Point>) {
263        self.layout.draw(ctx, origin)
264    }
265
266    /// Return the offset of the first baseline relative to the bottom of the widget.
267    pub fn baseline_offset(&self) -> f64 {
268        let text_metrics = self.layout.layout_metrics();
269        text_metrics.size.height - text_metrics.first_baseline
270    }
271}
272
273impl<T: TextStorage> Label<T> {
274    /// Create a new [`RawLabel`].
275    ///
276    /// This can display text `Data` directly.
277    pub fn raw() -> RawLabel<T> {
278        RawLabel::new()
279    }
280}
281
282impl<T: Data> Label<T> {
283    /// Construct a new `Label` widget.
284    ///
285    /// ```
286    /// use druid::LocalizedString;
287    /// use druid::widget::Label;
288    ///
289    /// // Construct a new Label using static string.
290    /// let _: Label<u32> = Label::new("Hello world");
291    ///
292    /// // Construct a new Label using localized string.
293    /// let text = LocalizedString::new("hello-counter").with_arg("count", |data: &u32, _env| (*data).into());
294    /// let _: Label<u32> = Label::new(text);
295    ///
296    /// // Construct a new dynamic Label. Text will be updated when data changes.
297    /// let _: Label<u32> = Label::new(|data: &u32, _env: &_| format!("Hello world: {}", data));
298    /// ```
299    pub fn new(text: impl Into<LabelText<T>>) -> Self {
300        let text = text.into();
301        let current_text = text.display_text();
302        Self {
303            text,
304            current_text,
305            label: RawLabel::new(),
306            text_should_be_updated: true,
307        }
308    }
309
310    /// Construct a new dynamic label.
311    ///
312    /// The contents of this label are generated from the data using a closure.
313    ///
314    /// This is provided as a convenience; a closure can also be passed to [`new`],
315    /// but due to limitations of the implementation of that method, the types in
316    /// the closure need to be annotated, which is not true for this method.
317    ///
318    /// # Examples
319    ///
320    /// The following are equivalent.
321    ///
322    /// ```
323    /// use druid::Env;
324    /// use druid::widget::Label;
325    /// let label1: Label<u32> = Label::new(|data: &u32, _: &Env| format!("total is {}", data));
326    /// let label2: Label<u32> = Label::dynamic(|data, _| format!("total is {}", data));
327    /// ```
328    ///
329    /// [`new`]: Label::new
330    pub fn dynamic(text: impl Fn(&T, &Env) -> String + 'static) -> Self {
331        let text: LabelText<T> = text.into();
332        Label::new(text)
333    }
334
335    /// Return the current value of the label's text.
336    pub fn text(&self) -> ArcStr {
337        self.text.display_text()
338    }
339
340    /// Set the label's text.
341    ///
342    /// # Note
343    ///
344    /// If you change this property, at runtime, you **must** ensure that [`update`]
345    /// is called in order to correctly recompute the text. If you are unsure,
346    /// call [`request_update`] explicitly.
347    ///
348    /// [`update`]: Widget::update
349    /// [`request_update`]: EventCtx::request_update
350    pub fn set_text(&mut self, text: impl Into<LabelText<T>>) {
351        self.text = text.into();
352        self.text_should_be_updated = true;
353    }
354
355    /// Builder-style method for setting the text color.
356    ///
357    /// The argument can be either a `Color` or a [`Key<Color>`].
358    ///
359    /// [`Key<Color>`]: crate::Key
360    pub fn with_text_color(mut self, color: impl Into<KeyOrValue<Color>>) -> Self {
361        self.label.set_text_color(color);
362        self
363    }
364
365    /// Builder-style method for setting the text size.
366    ///
367    /// The argument can be either an `f64` or a [`Key<f64>`].
368    ///
369    /// [`Key<f64>`]: crate::Key
370    pub fn with_text_size(mut self, size: impl Into<KeyOrValue<f64>>) -> Self {
371        self.label.set_text_size(size);
372        self
373    }
374
375    /// Builder-style method for setting the font.
376    ///
377    /// The argument can be a [`FontDescriptor`] or a [`Key<FontDescriptor>`]
378    /// that refers to a font defined in the [`Env`].
379    ///
380    /// [`Key<FontDescriptor>`]: crate::Key
381    pub fn with_font(mut self, font: impl Into<KeyOrValue<FontDescriptor>>) -> Self {
382        self.label.set_font(font);
383        self
384    }
385
386    /// Builder-style method to set the [`LineBreaking`] behaviour.
387    pub fn with_line_break_mode(mut self, mode: LineBreaking) -> Self {
388        self.label.set_line_break_mode(mode);
389        self
390    }
391
392    /// Builder-style method to set the [`TextAlignment`].
393    pub fn with_text_alignment(mut self, alignment: TextAlignment) -> Self {
394        self.label.set_text_alignment(alignment);
395        self
396    }
397
398    /// Draw this label's text at the provided `Point`, without internal padding.
399    ///
400    /// This is a convenience for widgets that want to use Label as a way
401    /// of managing a dynamic or localized string, but want finer control
402    /// over where the text is drawn.
403    pub fn draw_at(&self, ctx: &mut PaintCtx, origin: impl Into<Point>) {
404        self.label.draw_at(ctx, origin)
405    }
406}
407
408impl Static {
409    fn new(s: ArcStr) -> Self {
410        Static {
411            string: s,
412            resolved: false,
413        }
414    }
415
416    fn resolve(&mut self) -> bool {
417        let is_first_call = !self.resolved;
418        self.resolved = true;
419        is_first_call
420    }
421}
422
423impl<T> Dynamic<T> {
424    fn resolve(&mut self, data: &T, env: &Env) -> bool {
425        let new = (self.f)(data, env);
426        let changed = new != self.resolved;
427        self.resolved = new;
428        changed
429    }
430}
431
432impl<T: Data> LabelText<T> {
433    /// Call callback with the text that should be displayed.
434    pub fn with_display_text<V>(&self, mut cb: impl FnMut(&str) -> V) -> V {
435        match self {
436            LabelText::Static(s) => cb(&s.string),
437            LabelText::Localized(s) => cb(&s.localized_str()),
438            LabelText::Dynamic(s) => cb(&s.resolved),
439        }
440    }
441
442    /// Return the current resolved text.
443    pub fn display_text(&self) -> ArcStr {
444        match self {
445            LabelText::Static(s) => s.string.clone(),
446            LabelText::Localized(s) => s.localized_str(),
447            LabelText::Dynamic(s) => s.resolved.clone(),
448        }
449    }
450
451    /// Update the localization, if necessary.
452    /// This ensures that localized strings are up to date.
453    ///
454    /// Returns `true` if the string has changed.
455    pub fn resolve(&mut self, data: &T, env: &Env) -> bool {
456        match self {
457            LabelText::Static(s) => s.resolve(),
458            LabelText::Localized(s) => s.resolve(data, env),
459            LabelText::Dynamic(s) => s.resolve(data, env),
460        }
461    }
462}
463
464impl<T: Data> Widget<T> for Label<T> {
465    #[instrument(name = "Label", level = "trace", skip(self, _ctx, _event, _data, _env))]
466    fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut T, _env: &Env) {}
467
468    #[instrument(name = "Label", level = "trace", skip(self, ctx, event, data, env))]
469    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
470        if matches!(event, LifeCycle::WidgetAdded) {
471            self.text.resolve(data, env);
472            self.text_should_be_updated = false;
473        }
474        self.label
475            .lifecycle(ctx, event, &self.text.display_text(), env);
476    }
477
478    #[instrument(name = "Label", level = "trace", skip(self, ctx, _old_data, data, env))]
479    fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
480        let data_changed = self.text.resolve(data, env);
481        self.text_should_be_updated = false;
482        if data_changed {
483            let new_text = self.text.display_text();
484            self.label.update(ctx, &self.current_text, &new_text, env);
485            self.current_text = new_text;
486        } else if ctx.env_changed() {
487            self.label
488                .update(ctx, &self.current_text, &self.current_text, env);
489        }
490    }
491
492    #[instrument(name = "Label", level = "trace", skip(self, ctx, bc, _data, env))]
493    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &T, env: &Env) -> Size {
494        self.label.layout(ctx, bc, &self.current_text, env)
495    }
496
497    #[instrument(name = "Label", level = "trace", skip(self, ctx, _data, env))]
498    fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, env: &Env) {
499        if self.text_should_be_updated {
500            tracing::warn!("Label text changed without call to update. See LabelAdapter::set_text for information.");
501        }
502        self.label.paint(ctx, &self.current_text, env)
503    }
504
505    fn debug_state(&self, _data: &T) -> DebugState {
506        DebugState {
507            display_name: self.short_type_name().to_string(),
508            main_value: self.current_text.to_string(),
509            ..Default::default()
510        }
511    }
512
513    fn compute_max_intrinsic(
514        &mut self,
515        axis: Axis,
516        ctx: &mut LayoutCtx,
517        bc: &BoxConstraints,
518        _data: &T,
519        env: &Env,
520    ) -> f64 {
521        self.label
522            .compute_max_intrinsic(axis, ctx, bc, &self.current_text, env)
523    }
524}
525
526impl<T: TextStorage> Widget<T> for RawLabel<T> {
527    #[instrument(
528        name = "RawLabel",
529        level = "trace",
530        skip(self, ctx, event, _data, _env)
531    )]
532    fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut T, _env: &Env) {
533        match event {
534            Event::MouseUp(event) => {
535                // Account for the padding
536                let pos = event.pos - Vec2::new(LABEL_X_PADDING, 0.0);
537                if let Some(link) = self.layout.link_for_pos(pos) {
538                    ctx.submit_command(link.command.clone());
539                }
540            }
541            Event::MouseMove(event) => {
542                // Account for the padding
543                let pos = event.pos - Vec2::new(LABEL_X_PADDING, 0.0);
544
545                if self.layout.link_for_pos(pos).is_some() {
546                    ctx.set_cursor(&Cursor::Pointer);
547                } else {
548                    ctx.clear_cursor();
549                }
550            }
551            _ => {}
552        }
553    }
554
555    #[instrument(name = "RawLabel", level = "trace", skip(self, ctx, event, data, _env))]
556    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, _env: &Env) {
557        match event {
558            LifeCycle::WidgetAdded => {
559                self.layout.set_text(data.to_owned());
560            }
561            LifeCycle::DisabledChanged(disabled) => {
562                let color = if *disabled {
563                    KeyOrValue::Key(crate::theme::DISABLED_TEXT_COLOR)
564                } else {
565                    self.default_text_color.clone()
566                };
567                self.layout.set_text_color(color);
568                ctx.request_layout();
569            }
570            _ => {}
571        }
572    }
573
574    #[instrument(
575        name = "RawLabel",
576        level = "trace",
577        skip(self, ctx, old_data, data, _env)
578    )]
579    fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, _env: &Env) {
580        if !old_data.same(data) {
581            self.layout.set_text(data.clone());
582            ctx.request_layout();
583        }
584        if self.layout.needs_rebuild_after_update(ctx) {
585            ctx.request_layout();
586        }
587    }
588
589    #[instrument(name = "RawLabel", level = "trace", skip(self, ctx, bc, _data, env))]
590    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &T, env: &Env) -> Size {
591        bc.debug_check("Label");
592
593        let width = match self.line_break_mode {
594            LineBreaking::WordWrap => bc.max().width - LABEL_X_PADDING * 2.0,
595            _ => f64::INFINITY,
596        };
597
598        self.layout.set_wrap_width(width);
599        self.layout.rebuild_if_needed(ctx.text(), env);
600
601        let text_metrics = self.layout.layout_metrics();
602        ctx.set_baseline_offset(text_metrics.size.height - text_metrics.first_baseline);
603        let size = bc.constrain(Size::new(
604            text_metrics.size.width + 2. * LABEL_X_PADDING,
605            text_metrics.size.height,
606        ));
607        trace!("Computed size: {}", size);
608        size
609    }
610
611    #[instrument(name = "RawLabel", level = "trace", skip(self, ctx, _data, _env))]
612    fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, _env: &Env) {
613        let origin = Point::new(LABEL_X_PADDING, 0.0);
614        let label_size = ctx.size();
615
616        if self.line_break_mode == LineBreaking::Clip {
617            ctx.clip(label_size.to_rect());
618        }
619        self.draw_at(ctx, origin)
620    }
621
622    fn compute_max_intrinsic(
623        &mut self,
624        axis: Axis,
625        ctx: &mut LayoutCtx,
626        bc: &BoxConstraints,
627        data: &T,
628        env: &Env,
629    ) -> f64 {
630        match axis {
631            Axis::Horizontal => {
632                match self.line_break_mode {
633                    LineBreaking::WordWrap => {
634                        // Height is irrelevant for labels. So max preferred/intrinsic width of a label is the size
635                        // it'd take without any word wrapping.
636                        self.line_break_mode = LineBreaking::Clip;
637                        let s = self.layout(ctx, bc, data, env);
638                        self.line_break_mode = LineBreaking::WordWrap;
639                        s.width
640                    }
641                    _ => self.layout(ctx, bc, data, env).width,
642                }
643            }
644            Axis::Vertical => {
645                warn!("Max intrinsic height of a label is not implemented.");
646                0.
647            }
648        }
649    }
650}
651
652impl<T: TextStorage> Default for RawLabel<T> {
653    fn default() -> Self {
654        Self::new()
655    }
656}
657
658impl<T> Deref for Label<T> {
659    type Target = RawLabel<ArcStr>;
660    fn deref(&self) -> &Self::Target {
661        &self.label
662    }
663}
664
665impl<T> DerefMut for Label<T> {
666    fn deref_mut(&mut self) -> &mut Self::Target {
667        &mut self.label
668    }
669}
670impl<T> From<String> for LabelText<T> {
671    fn from(src: String) -> LabelText<T> {
672        LabelText::Static(Static::new(src.into()))
673    }
674}
675
676impl<T> From<&str> for LabelText<T> {
677    fn from(src: &str) -> LabelText<T> {
678        LabelText::Static(Static::new(src.into()))
679    }
680}
681
682impl<T> From<ArcStr> for LabelText<T> {
683    fn from(string: ArcStr) -> LabelText<T> {
684        LabelText::Static(Static::new(string))
685    }
686}
687
688impl<T> From<LocalizedString<T>> for LabelText<T> {
689    fn from(src: LocalizedString<T>) -> LabelText<T> {
690        LabelText::Localized(src)
691    }
692}
693
694impl<T, S, F> From<F> for LabelText<T>
695where
696    S: Into<Arc<str>>,
697    F: Fn(&T, &Env) -> S + 'static,
698{
699    fn from(src: F) -> LabelText<T> {
700        let f = Arc::new(move |state: &T, env: &Env| src(state, env).into());
701        LabelText::Dynamic(Dynamic {
702            f,
703            resolved: ArcStr::from(""),
704        })
705    }
706}