Skip to content

Commit

Permalink
Make TextBox placeholder translatable (#1908)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Swatinem authored Aug 15, 2021
1 parent ac34d2c commit f053464
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 25 deletions.
2 changes: 1 addition & 1 deletion druid/examples/textbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.";

Expand Down
68 changes: 44 additions & 24 deletions druid/src/widget/textbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));

Expand All @@ -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<T> {
placeholder: TextLayout<String>,
placeholder_text: LabelText<T>,
placeholder_layout: TextLayout<ArcStr>,
inner: Scroll<T, Padding<T, TextComponent<T>>>,
scroll_to_selection_after_layout: bool,
multiline: bool,
Expand All @@ -70,8 +73,9 @@ pub struct TextBox<T> {
impl<T: EditableText + TextStorage> TextBox<T> {
/// 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(),
Expand All @@ -81,7 +85,8 @@ impl<T: EditableText + TextStorage> TextBox<T> {
Self {
inner: scroll,
scroll_to_selection_after_layout: false,
placeholder,
placeholder_text,
placeholder_layout,
multiline: false,
was_focused_from_click: false,
cursor_on: false,
Expand Down Expand Up @@ -116,12 +121,6 @@ impl<T: EditableText + TextStorage> TextBox<T> {
}

impl<T> TextBox<T> {
/// Builder-style method to set the `TextBox`'s placeholder text.
pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> 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<f64>`].
Expand Down Expand Up @@ -178,11 +177,6 @@ impl<T> TextBox<T> {
self
}

/// Set the `TextBox`'s placeholder text.
pub fn set_placeholder(&mut self, placeholder: impl Into<String>) {
self.placeholder.set_text(placeholder.into());
}

/// Set the text size.
///
/// The argument can be either an `f64` or a [`Key<f64>`].
Expand All @@ -199,7 +193,7 @@ impl<T> TextBox<T> {
.borrow_mut()
.layout
.set_text_size(size.clone());
self.placeholder.set_text_size(size);
self.placeholder_layout.set_text_size(size);
}

/// Set the font.
Expand All @@ -217,7 +211,7 @@ impl<T> TextBox<T> {
}
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``.
Expand Down Expand Up @@ -275,6 +269,21 @@ impl<T> TextBox<T> {
}
}

impl<T: Data> TextBox<T> {
/// Builder-style method to set the `TextBox`'s placeholder text.
pub fn with_placeholder(mut self, placeholder: impl Into<LabelText<T>>) -> Self {
self.set_placeholder(placeholder);
self
}

/// Set the `TextBox`'s placeholder text.
pub fn set_placeholder(&mut self, placeholder: impl Into<LabelText<T>>) {
self.placeholder_text = placeholder.into();
self.placeholder_layout
.set_text(self.placeholder_text.display_text());
}
}

impl<T> TextBox<T> {
/// An immutable reference to the inner [`TextComponent`].
///
Expand Down Expand Up @@ -466,6 +475,9 @@ impl<T: TextStorage + EditableText> Widget<T> for TextBox<T> {
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 => {
Expand Down Expand Up @@ -505,8 +517,16 @@ impl<T: TextStorage + EditableText> Widget<T> for TextBox<T> {

#[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() {
Expand All @@ -525,14 +545,14 @@ impl<T: TextStorage + EditableText> Widget<T> for TextBox<T> {
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()
};
Expand Down Expand Up @@ -586,7 +606,7 @@ impl<T: TextStorage + EditableText> Widget<T> for TextBox<T> {
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
Expand All @@ -599,7 +619,7 @@ impl<T: TextStorage + EditableText> Widget<T> for TextBox<T> {
// 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));
})
}
Expand Down

0 comments on commit f053464

Please sign in to comment.