From ec8cf37bce599bf5d78861ec683c85df1aea5214 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Sun, 30 Aug 2020 12:46:39 -0400 Subject: [PATCH] Basic integration with new Piet text API This introduces a new type, TextLayout, that is intended to be a component used by widgets that wish to display text, and which exposes the functionality of the Piet text API. This commit is a checkpoint; it adds only a small amount of new functionality, and largely tries to keep things working as they do currently. --- druid-shell/Cargo.toml | 4 +- druid-shell/examples/perftest.rs | 4 +- druid/examples/custom_widget.rs | 26 +-- druid/examples/styled_text.rs | 27 ++-- druid/src/core.rs | 50 +++--- druid/src/env.rs | 4 +- druid/src/lib.rs | 2 +- druid/src/text/layout.rs | 265 +++++++++++++++++++++++++++++++ druid/src/text/mod.rs | 2 + druid/src/theme.rs | 30 ++-- druid/src/widget/checkbox.rs | 7 +- druid/src/widget/label.rs | 108 ++++++------- druid/src/widget/radio.rs | 11 +- druid/src/widget/switch.rs | 74 ++++----- druid/src/widget/textbox.rs | 186 ++++++++++++---------- 15 files changed, 528 insertions(+), 272 deletions(-) create mode 100644 druid/src/text/layout.rs diff --git a/druid-shell/Cargo.toml b/druid-shell/Cargo.toml index 892bc10ef9..2949dbfbdc 100644 --- a/druid-shell/Cargo.toml +++ b/druid-shell/Cargo.toml @@ -19,7 +19,7 @@ x11 = ["x11rb", "nix", "cairo-sys-rs"] [dependencies] # NOTE: When changing the piet or kurbo versions, ensure that # the kurbo version included in piet is compatible with the kurbo version specified here. -piet-common = "0.2.0-pre2" +piet-common = "0.2.0-pre3" kurbo = "0.6.3" log = "0.4.11" @@ -79,5 +79,5 @@ version = "0.3.44" features = ["Window", "MouseEvent", "CssStyleDeclaration", "WheelEvent", "KeyEvent", "KeyboardEvent"] [dev-dependencies] -piet-common = { version = "0.2.0-pre2", features = ["png"] } +piet-common = { version = "0.2.0-pre3", features = ["png"] } simple_logger = { version = "1.9.0", default-features = false } diff --git a/druid-shell/examples/perftest.rs b/druid-shell/examples/perftest.rs index 3e391249a7..e07ce4b7ef 100644 --- a/druid-shell/examples/perftest.rs +++ b/druid-shell/examples/perftest.rs @@ -68,7 +68,7 @@ impl WinHandler for PerfTest { self.last_time = now; let layout = piet .text() - .new_text_layout(&msg) + .new_text_layout(msg) .font(FontFamily::MONOSPACE, 15.0) .text_color(FG_COLOR) .build() @@ -80,7 +80,7 @@ impl WinHandler for PerfTest { let layout = piet .text() - .new_text_layout(&msg) + .new_text_layout(msg) .text_color(color) .font(FontFamily::MONOSPACE, 48.0) .build() diff --git a/druid/examples/custom_widget.rs b/druid/examples/custom_widget.rs index ef503f27a0..0523231893 100644 --- a/druid/examples/custom_widget.rs +++ b/druid/examples/custom_widget.rs @@ -15,9 +15,12 @@ //! An example of a custom drawing widget. use druid::kurbo::BezPath; -use druid::piet::{FontFamily, ImageFormat, InterpolationMode, Text, TextLayoutBuilder}; +use druid::piet::{FontFamily, ImageFormat, InterpolationMode}; use druid::widget::prelude::*; -use druid::{Affine, AppLauncher, Color, LocalizedString, Point, Rect, WindowDesc}; +use druid::{ + Affine, AppLauncher, Color, FontDescriptor, LocalizedString, Point, Rect, TextLayout, + WindowDesc, +}; struct CustomWidget; @@ -54,7 +57,7 @@ impl Widget for CustomWidget { // The paint method gets called last, after an event flow. // It goes event -> update -> layout -> paint, and each method can influence the next. // Basically, anything that changes the appearance of a widget causes a paint. - fn paint(&mut self, ctx: &mut PaintCtx, data: &String, _env: &Env) { + fn paint(&mut self, ctx: &mut PaintCtx, data: &String, env: &Env) { // Let's draw a picture with Piet! // Clear the whole widget with the color of your choice @@ -81,21 +84,18 @@ impl Widget for CustomWidget { let fill_color = Color::rgba8(0x00, 0x00, 0x00, 0x7F); ctx.fill(rect, &fill_color); - // Text is easy, if you ignore all these unwraps. Just pick a font and a size. - // Here's where we actually use the UI state - let layout = ctx - .text() - .new_text_layout(data) - .font(FontFamily::SYSTEM_UI, 24.0) - .text_color(fill_color) - .build() - .unwrap(); + // Text is easy; in real use TextLayout should be stored in the widget + // and reused. + let mut layout = TextLayout::new(data.as_str()); + layout.set_font(FontDescriptor::new(FontFamily::SERIF).with_size(24.0)); + layout.set_text_color(fill_color); + layout.rebuild_if_needed(&mut ctx.text(), env); // Let's rotate our text slightly. First we save our current (default) context: ctx.with_save(|ctx| { // Now we can rotate the context (or set a clip path, for instance): ctx.transform(Affine::rotate(0.1)); - ctx.draw_text(&layout, (80.0, 40.0)); + layout.draw(ctx, (80.0, 40.0)); }); // When we exit with_save, the original context's rotation is restored diff --git a/druid/examples/styled_text.rs b/druid/examples/styled_text.rs index 73b5103973..314f14089e 100644 --- a/druid/examples/styled_text.rs +++ b/druid/examples/styled_text.rs @@ -16,14 +16,13 @@ use druid::widget::{Checkbox, Flex, Label, MainAxisAlignment, Painter, Parse, Stepper, TextBox}; use druid::{ - theme, AppLauncher, ArcStr, Color, Data, Key, Lens, LensExt, LensWrap, LocalizedString, - PlatformError, RenderContext, Widget, WidgetExt, WindowDesc, + theme, AppLauncher, Color, Data, FontDescriptor, FontFamily, Key, Lens, LensExt, LensWrap, + LocalizedString, PlatformError, RenderContext, Widget, WidgetExt, WindowDesc, }; use std::fmt::Display; -// This is a custom key we'll use with Env to set and get our text size. -const MY_CUSTOM_TEXT_SIZE: Key = Key::new("styled_text.custom_text_size"); -const MY_CUSTOM_FONT: Key = Key::new("styled_text.custom_font"); +// This is a custom key we'll use with Env to set and get our font. +const MY_CUSTOM_FONT: Key = Key::new("styled_text.custom_font"); #[derive(Clone, Lens, Data)] struct AppData { @@ -73,7 +72,7 @@ fn ui_builder() -> impl Widget { }); // This is druid's default text style. - // It's set by theme::LABEL_COLOR, theme::TEXT_SIZE_NORMAL, and theme::FONT_NAME + // It's set by theme::LABEL_COLOR and theme::UI_FONT let label = Label::new(|data: &String, _env: &_| format!("Default: {}", data)).lens(AppData::text); @@ -88,19 +87,19 @@ fn ui_builder() -> impl Widget { // to the default font) let styled_label = Label::new(|data: &AppData, _env: &_| format!("{}", data)) .with_text_color(theme::PRIMARY_LIGHT) - .with_text_size(MY_CUSTOM_TEXT_SIZE) .with_font(MY_CUSTOM_FONT) .background(my_painter) .on_click(|_, data, _| { data.size *= 1.1; }) .env_scope(|env: &mut druid::Env, data: &AppData| { - env.set(MY_CUSTOM_TEXT_SIZE, data.size); - if data.mono { - env.set(MY_CUSTOM_FONT, "monospace"); + let new_font = if data.mono { + FontDescriptor::new(FontFamily::MONOSPACE) } else { - env.set(MY_CUSTOM_FONT, env.get(theme::FONT_NAME)); + FontDescriptor::new(FontFamily::SYSTEM_UI) } + .with_size(data.size); + env.set(MY_CUSTOM_FONT, new_font); }); let stepper = Stepper::new() @@ -118,7 +117,10 @@ fn ui_builder() -> impl Widget { let mono_checkbox = Checkbox::new("Monospace").lens(AppData::mono); - let input = TextBox::new().fix_width(200.0).lens(AppData::text); + let input = TextBox::new() + .with_text_size(38.0) + .fix_width(200.0) + .lens(AppData::text); Flex::column() .main_axis_alignment(MainAxisAlignment::Center) @@ -131,4 +133,5 @@ fn ui_builder() -> impl Widget { .with_child(mono_checkbox) .with_spacer(8.0) .with_child(input.padding(5.0)) + .debug_widget_id() } diff --git a/druid/src/core.rs b/druid/src/core.rs index ce543e47bd..2c3b7941fc 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -19,12 +19,11 @@ use std::collections::{HashMap, VecDeque}; use crate::bloom::Bloom; use crate::contexts::ContextState; use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size, Vec2}; -use crate::piet::{FontFamily, PietTextLayout, RenderContext, Text, TextLayout, TextLayoutBuilder}; use crate::util::ExtendDrain; use crate::{ BoxConstraints, Color, Command, Data, Env, Event, EventCtx, InternalEvent, InternalLifeCycle, - LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, Target, TimerToken, UpdateCtx, Widget, - WidgetId, + LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, RenderContext, Target, TextLayout, + TimerToken, UpdateCtx, Widget, WidgetId, }; /// Our queue type @@ -51,7 +50,7 @@ pub struct WidgetPod { env: Option, inner: W, // stashed layout so we don't recompute this when debugging - debug_widget_text: Option, + debug_widget_text: TextLayout, } /// Generic state for all widgets in the hierarchy. @@ -145,7 +144,7 @@ impl> WidgetPod { old_data: None, env: None, inner, - debug_widget_text: None, + debug_widget_text: TextLayout::new(""), } } @@ -429,7 +428,7 @@ impl> WidgetPod { } fn make_widget_id_layout_if_needed(&mut self, id: WidgetId, ctx: &mut PaintCtx, env: &Env) { - if self.debug_widget_text.is_none() { + if self.debug_widget_text.needs_rebuild() { // switch text color based on background, this is meh and that's okay let border_color = env.get_debug_color(id.to_raw()); let (r, g, b, _) = border_color.as_rgba8(); @@ -439,34 +438,29 @@ impl> WidgetPod { } else { Color::BLACK }; - + self.debug_widget_text.set_text_size(10.0); + self.debug_widget_text.set_text_color(text_color); let id_string = id.to_raw().to_string(); - self.debug_widget_text = ctx - .text() - .new_text_layout(&id_string) - .font(FontFamily::SYSTEM_UI, 10.0) - .text_color(text_color) - .build() - .ok(); + self.debug_widget_text.set_text(id_string); + self.debug_widget_text + .rebuild_if_needed(&mut ctx.text(), env); } } fn debug_paint_widget_ids(&self, ctx: &mut PaintCtx, env: &Env) { // we clone because we need to move it for paint_with_z_index let text = self.debug_widget_text.clone(); - if let Some(text) = text { - let text_size = text.size(); - let origin = ctx.size().to_vec2() - text_size.to_vec2(); - let border_color = env.get_debug_color(ctx.widget_id().to_raw()); - self.debug_paint_layout_bounds(ctx, env); - - ctx.paint_with_z_index(ctx.depth(), move |ctx| { - let origin = Point::new(origin.x.max(0.0), origin.y.max(0.0)); - let text_rect = Rect::from_origin_size(origin, text_size); - ctx.fill(text_rect, &border_color); - ctx.draw_text(&text, origin); - }) - } + let text_size = text.size(); + let origin = ctx.size().to_vec2() - text_size.to_vec2(); + let border_color = env.get_debug_color(ctx.widget_id().to_raw()); + self.debug_paint_layout_bounds(ctx, env); + + ctx.paint_with_z_index(ctx.depth(), move |ctx| { + let origin = Point::new(origin.x.max(0.0), origin.y.max(0.0)); + let text_rect = Rect::from_origin_size(origin, text_size); + ctx.fill(text_rect, &border_color); + text.draw(ctx, origin); + }) } fn debug_paint_layout_bounds(&self, ctx: &mut PaintCtx, env: &Env) { @@ -1007,7 +1001,7 @@ mod tests { state: &mut state, }; - let env = Env::default(); + let env = crate::theme::init(); widget.lifecycle(&mut ctx, &LifeCycle::WidgetAdded, &None, &env); assert!(ctx.widget_state.children.may_contain(&ID_1)); diff --git a/druid/src/env.rs b/druid/src/env.rs index cb494a817c..82171ad1c6 100644 --- a/druid/src/env.rs +++ b/druid/src/env.rs @@ -87,7 +87,7 @@ struct EnvImpl { /// /// [`ValueType`]: trait.ValueType.html /// [`Env`]: struct.Env.html -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Key { key: &'static str, value_type: PhantomData<*const T>, @@ -120,7 +120,7 @@ pub enum Value { /// /// [`Key`]: struct.Key.html /// [`Env`]: struct.Env.html -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum KeyOrValue { /// A concrete [`Value`] of type `T`. /// diff --git a/druid/src/lib.rs b/druid/src/lib.rs index 4f1aaf8dd0..7eeb35b272 100644 --- a/druid/src/lib.rs +++ b/druid/src/lib.rs @@ -197,7 +197,7 @@ pub use lens::{Lens, LensExt, LensWrap}; pub use localization::LocalizedString; pub use menu::{sys as platform_menus, ContextMenu, MenuDesc, MenuItem}; pub use mouse::MouseEvent; -pub use text::FontDescriptor; +pub use text::{FontDescriptor, TextLayout}; pub use widget::{Widget, WidgetExt, WidgetId}; pub use win_handler::DruidHandler; pub use window::{Window, WindowId}; diff --git a/druid/src/text/layout.rs b/druid/src/text/layout.rs new file mode 100644 index 0000000000..468879f861 --- /dev/null +++ b/druid/src/text/layout.rs @@ -0,0 +1,265 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A type for laying out, drawing, and interacting with text. + +use std::ops::Range; + +use crate::kurbo::{Line, Point, Rect, Size}; +use crate::piet::{ + Color, PietText, PietTextLayout, Text as _, TextAttribute, TextLayout as _, + TextLayoutBuilder as _, +}; +use crate::{ArcStr, Data, Env, FontDescriptor, KeyOrValue, PaintCtx, RenderContext}; + +/// A component for displaying text on screen. +/// +/// This is a type intended to be used by other widgets that display text. +/// It allows for the text itself as well as font and other styling information +/// to be set and modified. It wraps an inner layout object, and handles +/// invalidating and rebuilding it as required. +/// +/// This object is not valid until the [`rebuild_if_needed`] method has been +/// called. Additionally, this method must be called anytime the text or +/// other properties have changed, or if any items in the [`Env`] that are +/// referenced in this layout change. In general, you should just call this +/// method as part of your widget's `update` method. +/// +/// [`rebuild_if_needed`]: #method.rebuild_if_needed +/// [`Env`]: struct.Env.html +#[derive(Clone)] +pub struct TextLayout { + text: ArcStr, + font: KeyOrValue, + text_size_override: Option>, + text_color: KeyOrValue, + //FIXME: all this caching stuff can go away when we have a simple way of + // checking if something has changed in the env. + cached_text_color: Color, + cached_font: FontDescriptor, + // when set, this will be used to override the size in he font descriptor. + // This provides an easy way to change only the font size, while still + // using a `FontDescriptor` in the `Env`. + cached_text_size: Option, + // the underlying layout object. This is constructed lazily. + layout: Option, +} + +impl TextLayout { + /// Create a new `TextLayout` object. + /// + /// You do not provide the actual text at creation time; instead you pass + /// it in when calling [`rebuild_if_needed`]. + /// + /// [`rebuild_if_needed`]: #method.rebuild_if_needed + pub fn new(text: impl Into) -> Self { + TextLayout { + text: text.into(), + font: crate::theme::UI_FONT.into(), + cached_font: Default::default(), + text_color: crate::theme::LABEL_COLOR.into(), + cached_text_color: Color::BLACK, + text_size_override: None, + cached_text_size: None, + layout: None, + } + } + + /// Returns `true` if this layout needs to be rebuilt. + /// + /// This happens (for instance) after style attributes are modified. + /// + /// This does not account for things like the text changing, handling that + /// is the responsibility of the user. + pub fn needs_rebuild(&self) -> bool { + self.layout.is_none() + } + + /// Set the text to display. + pub fn set_text(&mut self, text: impl Into) { + self.text = text.into(); + self.layout = None; + } + + /// Set the default text color for this layout. + pub fn set_text_color(&mut self, color: impl Into>) { + self.text_color = color.into(); + self.layout = None; + } + + /// Set the default font. + /// + /// The argument is a [`FontDescriptor`] or a [`Key`] that + /// can be resolved from the [`Env`]. + /// + /// [`FontDescriptor`]: struct.FontDescriptor.html + /// [`Env`]: struct.Env.html + /// [`Key`]: struct.Key.html + pub fn set_font(&mut self, font: impl Into>) { + self.font = font.into(); + self.layout = None; + self.text_size_override = None; + } + + /// Set the font size. + /// + /// This overrides the size in the [`FontDescriptor`] provided to [`set_font`]. + /// + /// [`set_font`]: #method.set_font.html + /// [`FontDescriptor`]: struct.FontDescriptor.html + pub fn set_text_size(&mut self, size: impl Into>) { + self.text_size_override = Some(size.into()); + self.layout = None; + } + + /// The size of the laid-out text. + /// + /// This is not meaningful until [`rebuild_if_needed`] has been called. + /// + /// [`rebuild_if_needed`]: #method.rebuild_if_needed + pub fn size(&self) -> Size { + self.layout + .as_ref() + .map(|layout| layout.size()) + .unwrap_or_default() + } + + /// For a given `Point` (relative to this object's origin), returns index + /// into the underlying text of the nearest grapheme boundary. + pub fn text_position_for_point(&self, point: Point) -> usize { + self.layout + .as_ref() + .map(|layout| layout.hit_test_point(point).idx) + .unwrap_or_default() + } + + /// Given the utf-8 position of a character boundary in the underlying text, + /// return the `Point` (relative to this object's origin) representing the + /// boundary of the containing grapheme. + /// + /// # Panics + /// + /// Panics if `text_pos` is not a character boundary. + pub fn point_for_text_position(&self, text_pos: usize) -> Point { + self.layout + .as_ref() + .map(|layout| layout.hit_test_text_position(text_pos).point) + .unwrap_or_default() + } + + /// Given a utf-8 range in the underlying text, return a `Vec` of `Rect`s + /// representing the nominal bounding boxes of the text in that range. + /// + /// # Panics + /// + /// Panics if the range start or end is not a character boundary. + pub fn rects_for_range(&self, range: Range) -> Vec { + self.layout + .as_ref() + .map(|layout| layout.rects_for_range(range)) + .unwrap_or_default() + } + + /// Given the utf-8 position of a character boundary in the underlying text, + /// return a `Line` suitable for drawing a vertical cursor at that boundary. + pub fn cursor_line_for_text_position(&self, text_pos: usize) -> Line { + self.layout + .as_ref() + .map(|layout| { + let pos = layout.hit_test_text_position(text_pos); + let line_metrics = layout.line_metric(pos.line).unwrap(); + let p1 = (pos.point.x, line_metrics.y_offset); + let p2 = (pos.point.x, (line_metrics.y_offset + line_metrics.height)); + dbg!(Line::new(p1, p2)) + }) + .unwrap_or_else(|| Line::new(Point::ZERO, Point::ZERO)) + } + + /// Called during the containing widgets `update` method; this text object + /// will check to see if any used environment items have changed, + /// and invalidate itself as needed. + /// + /// Returns `true` if an item has changed, indicating that the text object + /// needs layout. + /// + /// # Note + /// + /// After calling this method, the layout may be invalid until the next call + /// to [`rebuild_layout_if_needed`], [`layout`], or [`paint`]. + /// + /// [`layout`]: #method.layout + /// [`paint`]: #method.paint + /// [`rebuild_layout_if_needed`]: #method.rebuild_layout_if_needed + pub fn rebuild_if_needed(&mut self, factory: &mut PietText, env: &Env) { + let new_font = self.font.resolve(env); + let new_color = self.text_color.resolve(env); + let new_size = self.text_size_override.as_ref().map(|key| key.resolve(env)); + + let needs_rebuild = !new_font.same(&self.cached_font) + || !new_color.same(&self.cached_text_color) + || new_size != self.cached_text_size + || self.layout.is_none(); + + self.cached_font = new_font; + self.cached_text_color = new_color; + self.cached_text_size = new_size; + + if needs_rebuild { + let descriptor = if let Some(size) = &self.cached_text_size { + self.cached_font.clone().with_size(*size) + } else { + self.cached_font.clone() + }; + let text_color = self.cached_text_color.clone(); + self.layout = Some( + factory + .new_text_layout(self.text.clone()) + .font(descriptor.family.clone(), descriptor.size) + .default_attribute(descriptor.weight) + .default_attribute(descriptor.style) + .default_attribute(TextAttribute::ForegroundColor(text_color)) + .build() + .unwrap(), + ) + } + } + + /// Draw the layout at the provided `Point`. + /// + /// You must call [`rebuild_if_needed`] at some point before you first + /// call this method. + /// + /// [`rebuild_if_needed`]: #method.rebuild_if_needed + pub fn draw(&self, ctx: &mut PaintCtx, point: impl Into) { + ctx.draw_text(self.layout.as_ref().unwrap(), point) + } +} + +impl std::fmt::Debug for TextLayout { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct("TextLayout") + .field("font", &self.font) + .field("text_size_override", &self.text_size_override) + .field("text_color", &self.text_color) + .field( + "layout", + if self.layout.is_some() { + &"Some" + } else { + &"None" + }, + ) + .finish() + } +} diff --git a/druid/src/text/mod.rs b/druid/src/text/mod.rs index 3ff83efa29..35617ae128 100644 --- a/druid/src/text/mod.rs +++ b/druid/src/text/mod.rs @@ -17,6 +17,7 @@ pub mod backspace; mod editable_text; mod font_descriptor; +mod layout; pub mod movement; pub mod selection; mod text_input; @@ -24,6 +25,7 @@ mod text_input; 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::movement::{movement, Movement}; pub use self::selection::Selection; pub use self::text_input::{BasicTextInput, EditAction, MouseAction, TextInput}; diff --git a/druid/src/theme.rs b/druid/src/theme.rs index 8012eeaaa3..4510008d00 100644 --- a/druid/src/theme.rs +++ b/druid/src/theme.rs @@ -17,7 +17,7 @@ #![allow(missing_docs)] use crate::piet::Color; -use crate::{ArcStr, Env, Key}; +use crate::{Env, FontDescriptor, FontFamily, Key}; pub const WINDOW_BACKGROUND_COLOR: Key = Key::new("window_background_color"); @@ -41,11 +41,13 @@ pub const SELECTION_COLOR: Key = Key::new("selection_color"); pub const SELECTION_TEXT_COLOR: Key = Key::new("selection_text_color"); pub const CURSOR_COLOR: Key = Key::new("cursor_color"); -pub const FONT_NAME: Key = Key::new("font_name"); pub const TEXT_SIZE_NORMAL: Key = Key::new("text_size_normal"); pub const TEXT_SIZE_LARGE: Key = Key::new("text_size_large"); pub const BASIC_WIDGET_HEIGHT: Key = Key::new("basic_widget_height"); +/// The default font for labels, buttons, text boxes, and other UI elements. +pub const UI_FONT: Key = Key::new("druid.builtin.ui-font-descriptor"); + /// The default minimum width for a 'wide' widget; a textbox, slider, progress bar, etc. pub const WIDE_WIDGET_WIDTH: Key = Key::new("druid.widgets.long-widget-width"); pub const BORDERED_WIDGET_HEIGHT: Key = Key::new("bordered_widget_height"); @@ -63,7 +65,7 @@ pub const SCROLLBAR_EDGE_WIDTH: Key = Key::new("scrollbar_edge_width"); /// An initial theme. pub fn init() -> Env { - let mut env = Env::default() + Env::default() .adding(WINDOW_BACKGROUND_COLOR, Color::rgb8(0x29, 0x29, 0x29)) .adding(LABEL_COLOR, Color::rgb8(0xf0, 0xf0, 0xea)) .adding(PLACEHOLDER_COLOR, Color::rgb8(0x80, 0x80, 0x80)) @@ -96,21 +98,9 @@ pub fn init() -> Env { .adding(SCROLLBAR_WIDTH, 8.) .adding(SCROLLBAR_PAD, 2.) .adding(SCROLLBAR_RADIUS, 5.) - .adding(SCROLLBAR_EDGE_WIDTH, 1.); - - #[cfg(target_os = "windows")] - { - env = env.adding(FONT_NAME, "Segoe UI"); - } - #[cfg(target_os = "macos")] - { - // Ideally this would be a reference to San Francisco, but Cairo's - // "toy text" API doesn't seem to be able to access it easily. - env = env.adding(FONT_NAME, "Arial"); - } - #[cfg(not(any(target_os = "macos", target_os = "windows")))] - { - env = env.adding(FONT_NAME, "sans-serif"); - } - env + .adding(SCROLLBAR_EDGE_WIDTH, 1.) + .adding( + UI_FONT, + FontDescriptor::new(FontFamily::SYSTEM_UI).with_size(15.0), + ) } diff --git a/druid/src/widget/checkbox.rs b/druid/src/widget/checkbox.rs index 0891f979a4..24ac045fcb 100644 --- a/druid/src/widget/checkbox.rs +++ b/druid/src/widget/checkbox.rs @@ -25,6 +25,7 @@ use crate::{ /// A checkbox that toggles a `bool`. pub struct Checkbox { + //FIXME: this should be a TextUi struct child_label: WidgetPod>>, } @@ -61,13 +62,15 @@ impl Widget for Checkbox { } } - fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &bool, _env: &Env) { + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &bool, env: &Env) { + self.child_label.lifecycle(ctx, event, data, env); if let LifeCycle::HotChanged(_) = event { ctx.request_paint(); } } - fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &bool, _data: &bool, _env: &Env) { + fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &bool, data: &bool, env: &Env) { + self.child_label.update(ctx, data, env); ctx.request_paint(); } diff --git a/druid/src/widget/label.rs b/druid/src/widget/label.rs index 688d663b35..9d94a3170a 100644 --- a/druid/src/widget/label.rs +++ b/druid/src/widget/label.rs @@ -14,13 +14,10 @@ //! A label widget. -use crate::piet::{ - Color, FontFamily, PietText, PietTextLayout, RenderContext, Text, TextLayout, - TextLayoutBuilder, UnitPoint, -}; +use crate::piet::{Color, PietText, UnitPoint}; use crate::{ - theme, ArcStr, BoxConstraints, Data, Env, Event, EventCtx, KeyOrValue, LayoutCtx, LifeCycle, - LifeCycleCtx, LocalizedString, PaintCtx, Point, Size, UpdateCtx, Widget, + BoxConstraints, Data, Env, Event, EventCtx, FontDescriptor, KeyOrValue, LayoutCtx, LifeCycle, + LifeCycleCtx, LocalizedString, PaintCtx, Point, Size, TextLayout, UpdateCtx, Widget, }; // added padding between the edges of the widget and the text. @@ -53,9 +50,10 @@ pub struct Dynamic { /// A label that displays some text. pub struct Label { text: LabelText, - color: KeyOrValue, - size: KeyOrValue, - font: KeyOrValue, + layout: TextLayout, + // if our text is manually changed we need to rebuild the layout + // before using it again. + needs_update_text: bool, } impl Label { @@ -77,11 +75,11 @@ impl Label { /// ``` pub fn new(text: impl Into>) -> Self { let text = text.into(); + let layout = TextLayout::new(text.display_text()); Self { text, - color: theme::LABEL_COLOR.into(), - size: theme::TEXT_SIZE_NORMAL.into(), - font: theme::FONT_NAME.into(), + layout, + needs_update_text: true, } } @@ -122,7 +120,7 @@ impl Label { /// /// [`Key`]: ../struct.Key.html pub fn with_text_color(mut self, color: impl Into>) -> Self { - self.color = color.into(); + self.set_text_color(color); self } @@ -132,31 +130,27 @@ impl Label { /// /// [`Key`]: ../struct.Key.html pub fn with_text_size(mut self, size: impl Into>) -> Self { - self.size = size.into(); + self.set_text_size(size); self } /// Builder-style method for setting the font. /// - /// The argument can be a `&str`, `String`, or [`Key<&str>`]. + /// The argument can be a [`FontDescriptor`] or a [`Key`] + /// that refers to a font defined in the [`Env`]. /// - /// [`Key<&str>`]: ../struct.Key.html - pub fn with_font(mut self, font: impl Into>) -> Self { - self.font = font.into(); + /// [`Env`]: ../struct.Env.html + /// [`FontDescriptor`]: ../struct.FontDescriptor.html + /// [`Key`]: ../struct.Key.html + pub fn with_font(mut self, font: impl Into>) -> Self { + self.set_font(font); self } - /// Set a new text. - /// - /// Takes an already resolved string as input. - /// - /// If you're looking for full [`LabelText`] support, - /// then you need to create a new [`Label`]. - /// - /// [`Label`]: #method.new - /// [`LabelText`]: enum.LabelText.html - pub fn set_text(&mut self, text: impl Into) { - self.text = LabelText::Specific(text.into()); + /// Set the label's text. + pub fn set_text(&mut self, text: impl Into>) { + self.text = text.into(); + self.needs_update_text = true; } /// Returns this label's current text. @@ -170,7 +164,7 @@ impl Label { /// /// [`Key`]: ../struct.Key.html pub fn set_text_color(&mut self, color: impl Into>) { - self.color = color.into(); + self.layout.set_text_color(color); } /// Set the text size. @@ -179,32 +173,28 @@ impl Label { /// /// [`Key`]: ../struct.Key.html pub fn set_text_size(&mut self, size: impl Into>) { - self.size = size.into(); + self.layout.set_text_size(size); } /// Set the font. /// - /// The argument can be a `&str`, `String`, or [`Key<&str>`]. + /// The argument can be a [`FontDescriptor`] or a [`Key`] + /// that refers to a font defined in the [`Env`]. /// - /// [`Key<&str>`]: ../struct.Key.html - pub fn set_font(&mut self, font: impl Into>) { - self.font = font.into(); + /// [`Env`]: ../struct.Env.html + /// [`FontDescriptor`]: ../struct.FontDescriptor.html + /// [`Key`]: ../struct.Key.html + pub fn set_font(&mut self, font: impl Into>) { + self.layout.set_font(font); } - fn get_layout(&mut self, t: &mut PietText, env: &Env) -> PietTextLayout { - let font_name = self.font.resolve(env); - let font_size = self.size.resolve(env); - let color = self.color.resolve(env); - - // TODO: caching of both the format and the layout - self.text.with_display_text(|text| { - let font = t.font_family(&font_name).unwrap_or(FontFamily::SYSTEM_UI); - t.new_text_layout(&text) - .font(font, font_size) - .text_color(color.clone()) - .build() - .unwrap() - }) + fn update_text_if_needed(&mut self, factory: &mut PietText, data: &T, env: &Env) { + if self.needs_update_text { + self.text.resolve(data, env); + self.layout.set_text(self.text.display_text()); + self.layout.rebuild_if_needed(factory, env); + self.needs_update_text = false; + } } } @@ -252,36 +242,34 @@ impl LabelText { impl Widget for Label { fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut T, _env: &Env) {} - fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { if let LifeCycle::WidgetAdded = event { - self.text.resolve(data, env); + self.update_text_if_needed(&mut ctx.text(), data, env); } } fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { - if !old_data.same(data) && self.text.resolve(data, env) { + if !old_data.same(data) | self.text.resolve(data, env) { ctx.request_layout(); } + //FIXME: this should only happen if the env has changed. + self.layout.rebuild_if_needed(&mut ctx.text(), env); } - 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 { bc.debug_check("Label"); - let text_layout = self.get_layout(&mut ctx.text(), env); - let text_size = text_layout.size(); + let text_size = self.layout.size(); bc.constrain(Size::new( text_size.width + 2. * LABEL_X_PADDING, text_size.height, )) } - fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, env: &Env) { - let text_layout = self.get_layout(&mut ctx.text(), env); - + fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, _env: &Env) { // Find the origin for the text let origin = Point::new(LABEL_X_PADDING, 0.0); - - ctx.draw_text(&text_layout, origin); + self.layout.draw(ctx, origin) } } diff --git a/druid/src/widget/radio.rs b/druid/src/widget/radio.rs index 9ca92ba547..eb1907fda6 100644 --- a/druid/src/widget/radio.rs +++ b/druid/src/widget/radio.rs @@ -43,6 +43,7 @@ impl RadioGroup { /// A single radio button pub struct Radio { variant: T, + //FIXME: this should be using a TextUi struct child_label: WidgetPod>>, } @@ -76,14 +77,18 @@ impl Widget for Radio { } } - fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &T, _env: &Env) { + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { + self.child_label.lifecycle(ctx, event, data, env); if let LifeCycle::HotChanged(_) = event { ctx.request_paint(); } } - fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, _data: &T, _env: &Env) { - ctx.request_paint(); + fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { + self.child_label.update(ctx, data, env); + if !old_data.same(data) { + ctx.request_paint(); + } } fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { diff --git a/druid/src/widget/switch.rs b/druid/src/widget/switch.rs index f09cbab3d3..9a8201606b 100644 --- a/druid/src/widget/switch.rs +++ b/druid/src/widget/switch.rs @@ -17,13 +17,11 @@ use std::time::Duration; use crate::kurbo::{Circle, Point, Shape, Size}; -use crate::piet::{ - FontFamily, LinearGradient, RenderContext, Text, TextLayout, TextLayoutBuilder, UnitPoint, -}; +use crate::piet::{LinearGradient, RenderContext, UnitPoint}; use crate::theme; use crate::{ - BoxConstraints, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, UpdateCtx, - Widget, + BoxConstraints, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, TextLayout, + UpdateCtx, Widget, }; const SWITCH_CHANGE_TIME: f64 = 0.2; @@ -31,12 +29,28 @@ const SWITCH_PADDING: f64 = 3.; const SWITCH_WIDTH_RATIO: f64 = 2.75; /// A switch that toggles a `bool`. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct Switch { knob_pos: Point, knob_hovered: bool, knob_dragged: bool, animation_in_progress: bool, + on_text: TextLayout, + off_text: TextLayout, +} + +impl Default for Switch { + fn default() -> Self { + Switch { + knob_pos: Point::ZERO, + knob_hovered: false, + knob_dragged: false, + animation_in_progress: false, + //TODO: use localized strings, also probably make these configurable? + on_text: TextLayout::new("ON"), + off_text: TextLayout::new("OFF"), + } + } } impl Switch { @@ -51,51 +65,27 @@ impl Switch { } fn paint_labels(&mut self, ctx: &mut PaintCtx, env: &Env, switch_width: f64) { - let font_name = env.get(theme::FONT_NAME); - let font_size = env.get(theme::TEXT_SIZE_NORMAL); - let text_color = env.get(theme::LABEL_COLOR); let switch_height = env.get(theme::BORDERED_WIDGET_HEIGHT); let knob_size = switch_height - 2. * SWITCH_PADDING; - let font = ctx - .text() - .font_family(&font_name) - .unwrap_or(FontFamily::SYSTEM_UI); - - // off/on labels - // TODO: use LocalizedString - let on_label_layout = ctx - .text() - .new_text_layout("ON") - .font(font.clone(), font_size) - .text_color(text_color.clone()) - .build() - .unwrap(); - - let off_label_layout = ctx - .text() - .new_text_layout("OFF") - .font(font, font_size) - .text_color(text_color) - .build() - .unwrap(); - - let label_y = (switch_height - on_label_layout.size().height).max(0.0) / 2.0; + let on_size = self.on_text.size(); + let off_size = self.off_text.size(); + + let label_y = (switch_height - on_size.height).max(0.0) / 2.0; let label_x_space = switch_width - knob_size - SWITCH_PADDING * 2.0; let off_pos = knob_size / 2. + SWITCH_PADDING; let knob_delta = self.knob_pos.x - off_pos; - let on_label_width = on_label_layout.size().width; + let on_label_width = on_size.width; let on_base_x_pos = -on_label_width - (label_x_space - on_label_width) / 2.0 + SWITCH_PADDING; let on_label_origin = Point::new(on_base_x_pos + knob_delta, label_y); - let off_base_x_pos = - knob_size + (label_x_space - off_label_layout.size().width) / 2.0 + SWITCH_PADDING; + let off_base_x_pos = knob_size + (label_x_space - off_size.width) / 2.0 + SWITCH_PADDING; let off_label_origin = Point::new(off_base_x_pos + knob_delta, label_y); - ctx.draw_text(&on_label_layout, on_label_origin); - ctx.draw_text(&off_label_layout, off_label_origin); + self.on_text.draw(ctx, on_label_origin); + self.off_text.draw(ctx, off_label_origin); } } @@ -177,13 +167,7 @@ impl Widget for Switch { } } - fn layout( - &mut self, - _layout_ctx: &mut LayoutCtx, - bc: &BoxConstraints, - _data: &bool, - env: &Env, - ) -> Size { + fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints, _: &bool, env: &Env) -> Size { let width = env.get(theme::BORDERED_WIDGET_HEIGHT) * SWITCH_WIDTH_RATIO; bc.constrain(Size::new(width, env.get(theme::BORDERED_WIDGET_HEIGHT))) } diff --git a/druid/src/widget/textbox.rs b/druid/src/widget/textbox.rs index a2b7084874..64ac487d50 100644 --- a/druid/src/widget/textbox.rs +++ b/druid/src/widget/textbox.rs @@ -16,26 +16,22 @@ use std::time::Duration; +use crate::widget::prelude::*; use crate::{ - Application, BoxConstraints, Cursor, Env, Event, EventCtx, HotKey, KbKey, LayoutCtx, LifeCycle, - LifeCycleCtx, PaintCtx, Selector, SysMods, TimerToken, UpdateCtx, Widget, + Application, BoxConstraints, Cursor, Data, Env, FontDescriptor, HotKey, KbKey, KeyOrValue, + Selector, SysMods, TimerToken, }; -use crate::kurbo::{Affine, Line, Point, Size, Vec2}; -use crate::piet::{ - FontFamily, PietText, PietTextLayout, RenderContext, Text, TextAttribute, TextLayout, - TextLayoutBuilder, -}; +use crate::kurbo::{Affine, Insets, Point, Size}; use crate::theme; use crate::text::{ movement, offset_for_delete_backwards, BasicTextInput, EditAction, EditableText, MouseAction, - Movement, Selection, TextInput, + Movement, Selection, TextInput, TextLayout, }; const BORDER_WIDTH: f64 = 1.; -const PADDING_TOP: f64 = 5.; -const PADDING_LEFT: f64 = 4.; +const TEXT_INSETS: Insets = Insets::new(4.0, 2.0, 0.0, 2.0); // we send ourselves this when we want to reset blink, which must be done in event. const RESET_BLINK: Selector = Selector::new("druid-builtin.reset-textbox-blink"); @@ -45,6 +41,7 @@ const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500); #[derive(Debug, Clone)] pub struct TextBox { placeholder: String, + text: TextLayout, width: f64, hscroll_offset: f64, selection: Selection, @@ -59,9 +56,11 @@ impl TextBox { /// Create a new TextBox widget pub fn new() -> TextBox { + let text = TextLayout::new(""); Self { width: 0.0, hscroll_offset: 0., + text, selection: Selection::caret(0), cursor_timer: TimerToken::INVALID, cursor_on: false, @@ -75,44 +74,48 @@ impl TextBox { self } - #[deprecated(since = "0.5.0", note = "Use TextBox::new instead")] - #[doc(hidden)] - pub fn raw() -> TextBox { - Self::new() + /// Builder-style method for setting the text size. + /// + /// The argument can be either an `f64` or a [`Key`]. + /// + /// [`Key`]: ../struct.Key.html + pub fn with_text_size(mut self, size: impl Into>) -> Self { + self.set_text_size(size); + self } - /// Calculate the PietTextLayout from the given text, font, and font size - fn get_layout( - &self, - piet_text: &mut PietText, - text: &str, - env: &Env, - use_placeholder_color: bool, - ) -> PietTextLayout { - let font_name = env.get(theme::FONT_NAME); - let font_size = env.get(theme::TEXT_SIZE_NORMAL); - let default_color = if use_placeholder_color { - env.get(theme::PLACEHOLDER_COLOR) - } else { - env.get(theme::LABEL_COLOR) - }; - let selection_text_color = env.get(theme::SELECTION_TEXT_COLOR); - - // TODO: caching of both the format and the layout - let font = piet_text - .font_family(&font_name) - .unwrap_or(FontFamily::SYSTEM_UI); - - piet_text - .new_text_layout(&text.to_string()) - .font(font, font_size) - .default_attribute(TextAttribute::ForegroundColor(default_color)) - .range_attribute( - self.selection.range(), - TextAttribute::ForegroundColor(selection_text_color), - ) - .build() - .unwrap() + /// Builder-style method for setting the font. + /// + /// The argument can be a [`FontDescriptor`] or a [`Key`] + /// that refers to a font defined in the [`Env`]. + /// + /// [`Env`]: ../struct.Env.html + /// [`FontDescriptor`]: ../struct.FontDescriptor.html + /// [`Key`]: ../struct.Key.html + pub fn with_font(mut self, font: impl Into>) -> Self { + self.set_font(font); + self + } + + /// Set the text size. + /// + /// The argument can be either an `f64` or a [`Key`]. + /// + /// [`Key`]: ../struct.Key.html + pub fn set_text_size(&mut self, size: impl Into>) { + self.text.set_text_size(size); + } + + /// Set the font. + /// + /// The argument can be a [`FontDescriptor`] or a [`Key`] + /// that refers to a font defined in the [`Env`]. + /// + /// [`Env`]: ../struct.Env.html + /// [`FontDescriptor`]: ../struct.FontDescriptor.html + /// [`Key`]: ../struct.Key.html + pub fn set_font(&mut self, font: impl Into>) { + self.text.set_font(font); } /// Insert text at the cursor position. @@ -206,26 +209,26 @@ impl TextBox { /// For a given point, returns the corresponding offset (in bytes) of /// the grapheme cluster closest to that point. - fn offset_for_point(&self, point: Point, layout: &PietTextLayout) -> usize { + fn offset_for_point(&self, point: Point) -> usize { // Translating from screenspace to Piet's text layout representation. // We need to account for hscroll_offset state and TextBox's padding. - let translated_point = Point::new(point.x + self.hscroll_offset - PADDING_LEFT, point.y); - let hit_test = layout.hit_test_point(translated_point); - hit_test.idx + let translated_point = Point::new(point.x + self.hscroll_offset - TEXT_INSETS.x0, point.y); + self.text.text_position_for_point(translated_point) } /// Given an offset (in bytes) of a valid grapheme cluster, return /// the corresponding x coordinate of that grapheme on the screen. - fn x_for_offset(&self, layout: &PietTextLayout, offset: usize) -> f64 { - layout.hit_test_text_position(offset).point.x + fn x_pos_for_offset(&self, offset: usize) -> f64 { + self.text.point_for_text_position(offset).x } /// Calculate a stateful scroll offset - fn update_hscroll(&mut self, layout: &PietTextLayout) { - let cursor_x = self.x_for_offset(layout, self.cursor()); - let overall_text_width = layout.size().width; + fn update_hscroll(&mut self) { + let cursor_x = self.x_pos_for_offset(self.cursor()); + let overall_text_width = self.text.size().width; - let padding = PADDING_LEFT * 2.; + // when advancing the cursor, we want some additional padding + let padding = TEXT_INSETS.x0 * 2.; if overall_text_width < self.width { // There's no offset if text is smaller than text box // @@ -257,8 +260,6 @@ impl Widget for TextBox { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut String, env: &Env) { // Guard against external changes in data? self.selection = self.selection.constrain_to(data); - - let mut text_layout = self.get_layout(&mut ctx.text(), &data, env, data.is_empty()); let mut edit_action = None; match event { @@ -267,7 +268,7 @@ impl Widget for TextBox { ctx.set_active(true); if !mouse.focus { - let cursor_offset = self.offset_for_point(mouse.pos, &text_layout); + let cursor_offset = self.offset_for_point(mouse.pos); edit_action = Some(EditAction::Click(MouseAction { row: 0, column: cursor_offset, @@ -280,7 +281,7 @@ impl Widget for TextBox { Event::MouseMove(mouse) => { ctx.set_cursor(&Cursor::IBeam); if ctx.is_active() { - let cursor_offset = self.offset_for_point(mouse.pos, &text_layout); + let cursor_offset = self.offset_for_point(mouse.pos); edit_action = Some(EditAction::Drag(MouseAction { row: 0, column: cursor_offset, @@ -359,36 +360,63 @@ impl Widget for TextBox { self.do_edit_action(edit_action, data); self.reset_cursor_blink(ctx); + self.text.set_text(data.as_str()); + self.text.rebuild_if_needed(&mut ctx.text(), env); if !is_select_all { - text_layout = self.get_layout(&mut ctx.text(), &data, env, data.is_empty()); - self.update_hscroll(&text_layout); + self.update_hscroll(); } } } - fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &String, _env: &Env) { + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &String, env: &Env) { match event { - LifeCycle::WidgetAdded => ctx.register_for_focus(), + LifeCycle::WidgetAdded => { + ctx.register_for_focus(); + self.text.set_text(data.clone()); + self.text.rebuild_if_needed(&mut ctx.text(), env); + } // an open question: should we be able to schedule timers here? LifeCycle::FocusChanged(true) => ctx.submit_command(RESET_BLINK, ctx.widget_id()), _ => (), } } - fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &String, _data: &String, _env: &Env) { + fn update(&mut self, ctx: &mut UpdateCtx, old_data: &String, data: &String, env: &Env) { + let content = if data.is_empty() { + &self.placeholder + } else { + data + }; + + // setting text color rebuilds layout, so don't do it if we don't have to + if !old_data.same(data) { + self.selection = self.selection.constrain_to(content); + self.text.set_text(data.as_str()); + if data.is_empty() { + self.text.set_text_color(theme::PLACEHOLDER_COLOR); + } else { + self.text.set_text_color(theme::LABEL_COLOR); + } + } + + self.text.rebuild_if_needed(&mut ctx.text(), env); ctx.request_paint(); } fn layout( &mut self, - _layout_ctx: &mut LayoutCtx, + _ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &String, env: &Env, ) -> Size { let width = env.get(theme::WIDE_WIDGET_WIDTH); - let height = env.get(theme::BORDERED_WIDGET_HEIGHT); + let min_height = env.get(theme::BORDERED_WIDGET_HEIGHT); + + let text_metrics = self.text.size(); + let text_height = text_metrics.height + TEXT_INSETS.y_value(); + let height = text_height.max(min_height); let size = bc.constrain((width, height)); self.width = size.width; @@ -405,7 +433,7 @@ impl Widget for TextBox { self.selection = self.selection.constrain_to(content); - let height = env.get(theme::BORDERED_WIDGET_HEIGHT); + let height = ctx.size().height; let background_color = env.get(theme::BACKGROUND_LIGHT); let selection_color = env.get(theme::SELECTION_COLOR); let cursor_color = env.get(theme::CURSOR_COLOR); @@ -431,36 +459,30 @@ impl Widget for TextBox { rc.clip(clip_rect); // Calculate layout - let text_layout = self.get_layout(&mut rc.text(), &content, env, data.is_empty()); - let text_size = text_layout.size(); + let text_size = self.text.size(); // Shift everything inside the clip by the hscroll_offset rc.transform(Affine::translate((-self.hscroll_offset, 0.))); // Layout, measure, and draw text - let top_padding = (height - text_size.height).min(PADDING_TOP).max(0.); - let text_pos = Point::new(PADDING_LEFT, top_padding); + let extra_padding = (height - text_size.height - TEXT_INSETS.y_value()).max(0.) / 2.; + let text_pos = Point::new(TEXT_INSETS.x0, TEXT_INSETS.y0 + extra_padding); // Draw selection rect if !self.selection.is_caret() { - for sel in text_layout.rects_for_range(self.selection.range()) { - let sel = sel + Vec2::new(PADDING_LEFT, top_padding); + for sel in self.text.rects_for_range(self.selection.range()) { + let sel = sel + text_pos.to_vec2(); let rounded = sel.to_rounded_rect(1.0); rc.fill(rounded, &selection_color); } } - rc.draw_text(&text_layout, text_pos); + self.text.draw(rc, text_pos); // Paint the cursor if focused and there's no selection if is_focused && self.cursor_on { - let pos = text_layout.hit_test_text_position(self.cursor()); - let metrics = text_layout.line_metric(pos.line).unwrap(); - //let cursor_x = self.x_for_offset(&text_layout, self.cursor()); - let xy = text_pos + Vec2::new(pos.point.x, 0.0); - let x2y2 = xy + Vec2::new(0., metrics.height); - let line = Line::new(xy, x2y2); - + let line = self.text.cursor_line_for_text_position(self.cursor()); + let line = line + text_pos.to_vec2(); rc.stroke(line, &cursor_color, 1.); } });