diff --git a/CHANGELOG.md b/CHANGELOG.md index 1613f2920b..71c5a319f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ You can find its changes [documented below](#060---2020-06-01). - `Event::should_propagate_to_hidden` and `Lifecycle::should_propagate_to_hidden` to determine whether an event should be sent to hidden widgets (e.g. in `Tabs` or `Either`). ([#1351] by [@andrewhickman]) - `set_cursor` can be called in the `update` method. ([#1361] by [@jneem]) - `WidgetPod::is_initialized` to check if a widget has received `WidgetAdded`. ([#1259] by [@finnerale]) +- `TextBox::with_text_alignment` and `TextBox::set_text_alignment` ([#1371] by [@cmyr]) ### Changed @@ -529,6 +530,7 @@ Last release without a changelog :( [#1351]: https://github.com/linebender/druid/pull/1351 [#1259]: https://github.com/linebender/druid/pull/1259 [#1361]: https://github.com/linebender/druid/pull/1361 +[#1371]: https://github.com/linebender/druid/pull/1371 [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/text/mod.rs b/druid/src/text/mod.rs index 8d870954f4..babffe4c0f 100644 --- a/druid/src/text/mod.rs +++ b/druid/src/text/mod.rs @@ -29,7 +29,7 @@ pub use self::attribute::{Attribute, AttributeSpans}; pub use self::backspace::offset_for_delete_backwards; pub use self::editable_text::{EditableText, EditableTextCursor, StringCursor}; pub use self::font_descriptor::FontDescriptor; -pub use self::layout::TextLayout; +pub use self::layout::{LayoutMetrics, TextLayout}; pub use self::movement::{movement, Movement}; pub use self::selection::Selection; pub use self::text_input::{BasicTextInput, EditAction, MouseAction, TextInput}; diff --git a/druid/src/widget/textbox.rs b/druid/src/widget/textbox.rs index 9c4ac86a86..e0f884d5a1 100644 --- a/druid/src/widget/textbox.rs +++ b/druid/src/widget/textbox.rs @@ -18,12 +18,13 @@ use std::time::Duration; use crate::kurbo::Vec2; use crate::text::{ - BasicTextInput, EditAction, EditableText, Editor, TextInput, TextLayout, TextStorage, + BasicTextInput, EditAction, EditableText, Editor, LayoutMetrics, TextInput, TextLayout, + TextStorage, }; use crate::widget::prelude::*; use crate::{ theme, Affine, Color, Cursor, FontDescriptor, HotKey, KbKey, KeyOrValue, Point, Selector, - SysMods, TimerToken, + SysMods, TextAlignment, TimerToken, }; const MAC_OR_LINUX: bool = cfg!(any(target_os = "macos", target_os = "linux")); @@ -42,6 +43,8 @@ pub struct TextBox { cursor_timer: TimerToken, cursor_on: bool, multiline: bool, + alignment: TextAlignment, + alignment_offset: f64, /// true if a click event caused us to gain focus. /// /// On macOS, if focus happens via click then we set the selection based @@ -70,6 +73,8 @@ impl TextBox { cursor_on: false, placeholder, multiline: false, + alignment: TextAlignment::Start, + alignment_offset: 0.0, was_focused_from_click: false, } } @@ -98,6 +103,29 @@ impl TextBox { self } + /// Builder-style method to set the [`TextAlignment`]. + /// + /// This is only relevant when the `TextBox` is *not* [`multiline`], + /// in which case it determines how the text is positioned inside the + /// `TextBox` when it does not fill the available space. + /// + /// # Note: + /// + /// This does not behave exactly like [`TextAlignment`] does when used + /// with label; in particular this does not account for reading direction. + /// This means that `TextAlignment::Start` (the default) always means + /// *left aligned*, and `TextAlignment::End` always means *right aligned*. + /// + /// This should be considered a bug, but it will not be fixed until proper + /// BiDi support is implemented. + /// + /// [`TextAlignment`]: enum.TextAlignment.html + /// [`multiline`]: #method.multiline + pub fn with_text_alignment(mut self, alignment: TextAlignment) -> Self { + self.set_text_alignment(alignment); + self + } + /// Builder-style method for setting the font. /// /// The argument can be a [`FontDescriptor`] or a [`Key`] @@ -146,6 +174,28 @@ impl TextBox { self.placeholder.set_font(font); } + /// Set the [`TextAlignment`] for this `TextBox``. + /// + /// This is only relevant when the `TextBox` is *not* [`multiline`], + /// in which case it determines how the text is positioned inside the + /// `TextBox` when it does not fill the available space. + /// + /// # Note: + /// + /// This does not behave exactly like [`TextAlignment`] does when used + /// with label; in particular this does not account for reading direction. + /// This means that `TextAlignment::Start` (the default) always means + /// *left aligned*, and `TextAlignment::End` always means *right aligned*. + /// + /// This should be considered a bug, but it will not be fixed until proper + /// BiDi support is implemented. + /// + /// [`TextAlignment`]: enum.TextAlignment.html + /// [`multiline`]: #method.multiline + pub fn set_text_alignment(&mut self, alignment: TextAlignment) { + self.alignment = alignment; + } + /// Set the text color. /// /// The argument can be either a `Color` or a [`Key`]. @@ -175,6 +225,7 @@ impl TextBox { let overall_text_width = self.editor.layout().size().width; let text_insets = env.get(theme::TEXTBOX_INSETS); + //// when advancing the cursor, we want some additional padding if overall_text_width < self_width - text_insets.x_value() { // There's no offset if text is smaller than text box // @@ -217,18 +268,30 @@ impl TextBox { fn should_draw_cursor(&self) -> bool { self.cursor_on } + + fn update_alignment_adjustment(&mut self, available_width: f64, metrics: &LayoutMetrics) { + self.alignment_offset = if self.multiline { + 0.0 + } else { + let extra_space = (available_width - metrics.size.width).max(0.0); + match self.alignment { + TextAlignment::Start | TextAlignment::Justified => 0.0, + TextAlignment::End => extra_space, + TextAlignment::Center => extra_space / 2.0, + } + } + } } impl Widget for TextBox { - fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, _env: &Env) { self.suppress_adjust_hscroll = false; match event { Event::MouseDown(mouse) => { ctx.request_focus(); ctx.set_active(true); let mut mouse = mouse.clone(); - let text_insets = env.get(theme::TEXTBOX_INSETS); - mouse.pos += Vec2::new(self.hscroll_offset - text_insets.x0, 0.0); + mouse.pos += Vec2::new(self.hscroll_offset - self.alignment_offset, 0.0); if !mouse.focus { self.was_focused_from_click = true; @@ -240,8 +303,7 @@ impl Widget for TextBox { } Event::MouseMove(mouse) => { let mut mouse = mouse.clone(); - let text_insets = env.get(theme::TEXTBOX_INSETS); - mouse.pos += Vec2::new(self.hscroll_offset - text_insets.x0, 0.0); + mouse.pos += Vec2::new(self.hscroll_offset - self.alignment_offset, 0.0); ctx.set_cursor(&Cursor::IBeam); if ctx.is_active() { self.editor.drag(&mouse, data); @@ -330,7 +392,7 @@ impl Widget for TextBox { } } - fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &T, env: &Env) -> Size { + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { let width = env.get(theme::WIDE_WIDGET_WIDTH); let text_insets = env.get(theme::TEXTBOX_INSETS); @@ -341,10 +403,17 @@ impl Widget for TextBox { } self.editor.rebuild_if_needed(ctx.text(), env); - let text_metrics = self.editor.layout().layout_metrics(); - let height = text_metrics.size.height + text_insets.y_value(); + let text_metrics = if data.is_empty() { + self.placeholder.layout_metrics() + } else { + self.editor.layout().layout_metrics() + }; + let height = text_metrics.size.height + text_insets.y_value(); let size = bc.constrain((width, height)); + // if we have a non-left text-alignment, we need to manually adjust our position. + self.update_alignment_adjustment(size.width - text_insets.x_value(), &text_metrics); + let bottom_padding = (size.height - text_metrics.size.height) / 2.0; let baseline_off = bottom_padding + (text_metrics.size.height - text_metrics.first_baseline); @@ -384,8 +453,7 @@ impl Widget for TextBox { // Shift everything inside the clip by the hscroll_offset rc.transform(Affine::translate((-self.hscroll_offset, 0.))); - let text_pos = Point::new(text_insets.x0, text_insets.y0); - + let text_pos = Point::new(text_insets.x0 + self.alignment_offset, text_insets.y0); // Draw selection rect if !data.is_empty() { if is_focused { @@ -402,14 +470,26 @@ impl Widget for TextBox { // Paint the cursor if focused and there's no selection if is_focused && self.should_draw_cursor() { - // the cursor position can extend past the edge of the layout - // (commonly when there is trailing whitespace) so we clamp it - // to the right edge. - let mut cursor = self.editor.cursor_line() + text_pos.to_vec2(); - let dx = size.width + self.hscroll_offset - text_insets.x1 - cursor.p0.x; - if dx < 0.0 { - cursor = cursor + Vec2::new(dx, 0.); - } + // if there's no data, we always draw the cursor based on + // our alignment. + let cursor = if data.is_empty() { + let dx = match self.alignment { + TextAlignment::Start | TextAlignment::Justified => text_insets.x0, + TextAlignment::Center => size.width / 2.0, + TextAlignment::End => size.width - text_insets.x1, + }; + self.editor.cursor_line() + Vec2::new(dx, text_insets.y0) + } else { + // the cursor position can extend past the edge of the layout + // (commonly when there is trailing whitespace) so we clamp it + // to the right edge. + let mut cursor = self.editor.cursor_line() + text_pos.to_vec2(); + let dx = size.width + self.hscroll_offset - text_insets.x0 - cursor.p0.x; + if dx < 0.0 { + cursor = cursor + Vec2::new(dx, 0.); + } + cursor + }; rc.stroke(cursor, &cursor_color, 1.); } });