Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support text alignment in single-line TextBox #1371

Merged
merged 1 commit into from
Nov 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion druid/src/text/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
120 changes: 100 additions & 20 deletions druid/src/widget/textbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand All @@ -42,6 +43,8 @@ pub struct TextBox<T> {
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
Expand Down Expand Up @@ -70,6 +73,8 @@ impl<T> TextBox<T> {
cursor_on: false,
placeholder,
multiline: false,
alignment: TextAlignment::Start,
alignment_offset: 0.0,
was_focused_from_click: false,
}
}
Expand Down Expand Up @@ -98,6 +103,29 @@ impl<T> TextBox<T> {
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<FontDescriptor>`]
Expand Down Expand Up @@ -146,6 +174,28 @@ impl<T> TextBox<T> {
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<Color>`].
Expand Down Expand Up @@ -175,6 +225,7 @@ impl<T: TextStorage + EditableText> TextBox<T> {
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
//
Expand Down Expand Up @@ -217,18 +268,30 @@ impl<T: TextStorage + EditableText> TextBox<T> {
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<T: TextStorage + EditableText> Widget<T> for TextBox<T> {
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;
Expand All @@ -240,8 +303,7 @@ impl<T: TextStorage + EditableText> Widget<T> for TextBox<T> {
}
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);
Expand Down Expand Up @@ -330,7 +392,7 @@ impl<T: TextStorage + EditableText> Widget<T> for TextBox<T> {
}
}

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);

Expand All @@ -341,10 +403,17 @@ impl<T: TextStorage + EditableText> Widget<T> for TextBox<T> {
}
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);
Expand Down Expand Up @@ -384,8 +453,7 @@ impl<T: TextStorage + EditableText> Widget<T> for TextBox<T> {
// 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 {
Expand All @@ -402,14 +470,26 @@ impl<T: TextStorage + EditableText> Widget<T> for TextBox<T> {

// 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.);
}
});
Expand Down