From 69e480fed9d8eedd411ca66949c79b1d474cdef0 Mon Sep 17 00:00:00 2001 From: Arpad Borsos Date: Tue, 10 Aug 2021 09:55:56 +0200 Subject: [PATCH] Make TextBox placeholder translatable This makes the `TextBox` placeholder a `LabelText` which can be initialized with a `LocalizedString` and thus translated. One limitation is that the `LabelText` is bound to the same `Data` type parameter as the lensed `TextBox`, so it is not possible currently to use translation parameters from the broader application state. --- druid/examples/textbox.rs | 2 +- druid/src/widget/textbox.rs | 68 ++++++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/druid/examples/textbox.rs b/druid/examples/textbox.rs index 4dd332516e..8fbcbe603a 100644 --- a/druid/examples/textbox.rs +++ b/druid/examples/textbox.rs @@ -34,7 +34,7 @@ const EXPLAINER: &str = "\ This example demonstrates some of the possible configurations \ of the TextBox widget.\n\ The top textbox allows a single line of input, with horizontal scrolling \ - but no scrollbars. The bottom textbox allows mutliple lines of text, wrapping \ + but no scrollbars. The bottom textbox allows multiple lines of text, wrapping \ words to fit the width, and allowing vertical scrolling when it runs out \ of room to grow vertically."; diff --git a/druid/src/widget/textbox.rs b/druid/src/widget/textbox.rs index 21b816c886..05c7e5648c 100644 --- a/druid/src/widget/textbox.rs +++ b/druid/src/widget/textbox.rs @@ -25,10 +25,12 @@ use crate::text::{ use crate::widget::prelude::*; use crate::widget::{Padding, Scroll, WidgetWrapper}; use crate::{ - theme, Color, Command, FontDescriptor, HotKey, KeyEvent, KeyOrValue, Point, Rect, SysMods, - TextAlignment, TimerToken, Vec2, + theme, ArcStr, Color, Command, FontDescriptor, HotKey, KeyEvent, KeyOrValue, Point, Rect, + SysMods, TextAlignment, TimerToken, Vec2, }; +use super::LabelText; + const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500); const MAC_OR_LINUX: bool = cfg!(any(target_os = "macos", target_os = "linux")); @@ -47,7 +49,8 @@ const SCROLL_TO_INSETS: Insets = Insets::uniform_xy(40.0, 0.0); /// [`Formatter`]: crate::text::format::Formatter /// [`ValueTextBox`]: super::ValueTextBox pub struct TextBox { - placeholder: TextLayout, + placeholder_text: LabelText, + placeholder_layout: TextLayout, inner: Scroll>>, scroll_to_selection_after_layout: bool, multiline: bool, @@ -70,8 +73,9 @@ pub struct TextBox { impl TextBox { /// Create a new TextBox widget. pub fn new() -> Self { - let mut placeholder = TextLayout::from_text(""); - placeholder.set_text_color(theme::PLACEHOLDER_COLOR); + let mut placeholder_layout = TextLayout::new(); + placeholder_layout.set_text_color(theme::PLACEHOLDER_COLOR); + let placeholder_text = "".into(); let mut scroll = Scroll::new(Padding::new( theme::TEXTBOX_INSETS, TextComponent::default(), @@ -81,7 +85,8 @@ impl TextBox { Self { inner: scroll, scroll_to_selection_after_layout: false, - placeholder, + placeholder_text, + placeholder_layout, multiline: false, was_focused_from_click: false, cursor_on: false, @@ -116,12 +121,6 @@ impl TextBox { } impl TextBox { - /// Builder-style method to set the `TextBox`'s placeholder text. - pub fn with_placeholder(mut self, placeholder: impl Into) -> Self { - self.placeholder.set_text(placeholder.into()); - self - } - /// Builder-style method for setting the text size. /// /// The argument can be either an `f64` or a [`Key`]. @@ -178,11 +177,6 @@ impl TextBox { self } - /// Set the `TextBox`'s placeholder text. - pub fn set_placeholder(&mut self, placeholder: impl Into) { - self.placeholder.set_text(placeholder.into()); - } - /// Set the text size. /// /// The argument can be either an `f64` or a [`Key`]. @@ -199,7 +193,7 @@ impl TextBox { .borrow_mut() .layout .set_text_size(size.clone()); - self.placeholder.set_text_size(size); + self.placeholder_layout.set_text_size(size); } /// Set the font. @@ -217,7 +211,7 @@ impl TextBox { } let font = font.into(); self.text_mut().borrow_mut().layout.set_font(font.clone()); - self.placeholder.set_font(font); + self.placeholder_layout.set_font(font); } /// Set the [`TextAlignment`] for this `TextBox``. @@ -275,6 +269,21 @@ impl TextBox { } } +impl TextBox { + /// Builder-style method to set the `TextBox`'s placeholder text. + pub fn with_placeholder(mut self, placeholder: impl Into>) -> Self { + self.set_placeholder(placeholder); + self + } + + /// Set the `TextBox`'s placeholder text. + pub fn set_placeholder(&mut self, placeholder: impl Into>) { + self.placeholder_text = placeholder.into(); + self.placeholder_layout + .set_text(self.placeholder_text.display_text()); + } +} + impl TextBox { /// An immutable reference to the inner [`TextComponent`]. /// @@ -466,6 +475,9 @@ impl Widget for TextBox { fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { match event { LifeCycle::WidgetAdded => { + if matches!(event, LifeCycle::WidgetAdded) { + self.placeholder_text.resolve(data, env); + } ctx.register_text_input(self.text().input_handler()); } LifeCycle::BuildFocusChain => { @@ -505,8 +517,16 @@ impl Widget for TextBox { #[instrument(name = "TextBox", level = "trace", skip(self, ctx, old, data, env))] fn update(&mut self, ctx: &mut UpdateCtx, old: &T, data: &T, env: &Env) { + let placeholder_changed = self.placeholder_text.resolve(data, env); + if placeholder_changed { + let new_text = self.placeholder_text.display_text(); + self.placeholder_layout.set_text(new_text); + } + self.inner.update(ctx, old, data, env); - if ctx.env_changed() && self.placeholder.needs_rebuild_after_update(ctx) { + if placeholder_changed + || (ctx.env_changed() && self.placeholder_layout.needs_rebuild_after_update(ctx)) + { ctx.request_layout(); } if self.text().can_write() { @@ -525,14 +545,14 @@ impl Widget for TextBox { let min_width = env.get(theme::WIDE_WIDGET_WIDTH); let textbox_insets = env.get(theme::TEXTBOX_INSETS); - self.placeholder.rebuild_if_needed(ctx.text(), env); + self.placeholder_layout.rebuild_if_needed(ctx.text(), env); let min_size = bc.constrain((min_width, 0.0)); let child_bc = BoxConstraints::new(min_size, bc.max()); let size = self.inner.layout(ctx, &child_bc, data, env); let text_metrics = if !self.text().can_read() || data.is_empty() { - self.placeholder.layout_metrics() + self.placeholder_layout.layout_metrics() } else { self.text().borrow().layout.layout_metrics() }; @@ -586,7 +606,7 @@ impl Widget for TextBox { if !data.is_empty() { self.inner.paint(ctx, data, env); } else { - let text_width = self.placeholder.layout_metrics().size.width; + let text_width = self.placeholder_layout.layout_metrics().size.width; let extra_width = (size.width - text_width - textbox_insets.x_value()).max(0.); let alignment = self.text().borrow().text_alignment(); // alignment is only used for single-line text boxes @@ -599,7 +619,7 @@ impl Widget for TextBox { // clip when we draw the placeholder, since it isn't in a clipbox ctx.with_save(|ctx| { ctx.clip(clip_rect); - self.placeholder + self.placeholder_layout .draw(ctx, (textbox_insets.x0 + x_offset, textbox_insets.y0)); }) }