Skip to content

Commit ef8f772

Browse files
committed
Basic rich text support
This adds a TextStorage trait for types that... store text. On top of this, it implements a RichText type, that is a string and a set of style spans. This type is currently immutable, in the sense that it cannot be edited. Editing is something that we would definitely like, at some point, but it expands the scope of this work significantly, and at the very least should be a separate patch.
1 parent c24b345 commit ef8f772

File tree

13 files changed

+251
-103
lines changed

13 files changed

+251
-103
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ You can find its changes [documented below](#060---2020-06-01).
4242
- WIDGET_PADDING items added to theme and `Flex::with_default_spacer`/`Flex::add_default_spacer` ([#1220] by [@cmyr])
4343
- CONFIGURE_WINDOW command to allow reconfiguration of an existing window. ([#1235] by [@rjwittams])
4444
- `RawLabel` widget displays text `Data`. ([#1252] by [@cmyr])
45+
- `RichText` and `Attribute` types for creating rich text ([#1255] by [@cmyr])
4546

4647
### Changed
4748

@@ -469,6 +470,7 @@ Last release without a changelog :(
469470
[#1245]: https://github.com/linebender/druid/pull/1245
470471
[#1251]: https://github.com/linebender/druid/pull/1251
471472
[#1252]: https://github.com/linebender/druid/pull/1252
473+
[#1255]: https://github.com/linebender/druid/pull/1255
472474

473475
[Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master
474476
[0.6.0]: https://github.com/linebender/druid/compare/v0.5.0...v0.6.0

druid/examples/custom_widget.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use druid::kurbo::BezPath;
1818
use druid::piet::{FontFamily, ImageFormat, InterpolationMode};
1919
use druid::widget::prelude::*;
2020
use druid::{
21-
Affine, AppLauncher, Color, FontDescriptor, LocalizedString, Point, Rect, TextLayout,
21+
Affine, AppLauncher, ArcStr, Color, FontDescriptor, LocalizedString, Point, Rect, TextLayout,
2222
WindowDesc,
2323
};
2424

@@ -86,7 +86,7 @@ impl Widget<String> for CustomWidget {
8686

8787
// Text is easy; in real use TextLayout should be stored in the widget
8888
// and reused.
89-
let mut layout = TextLayout::new(data.as_str());
89+
let mut layout = TextLayout::<ArcStr>::with_text(data.to_owned());
9090
layout.set_font(FontDescriptor::new(FontFamily::SERIF).with_size(24.0));
9191
layout.set_text_color(fill_color);
9292
layout.rebuild_if_needed(ctx.text(), env);

druid/examples/text.rs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414

1515
//! An example of various text layout features.
1616
17-
use druid::widget::{Controller, Flex, Label, LineBreaking, RadioGroup, Scroll};
17+
use druid::piet::{PietTextLayoutBuilder, TextStorage as PietTextStorage};
18+
use druid::text::{Attribute, RichText, TextStorage};
19+
use druid::widget::prelude::*;
20+
use druid::widget::{Controller, Flex, Label, LineBreaking, RadioGroup, RawLabel, Scroll};
1821
use druid::{
19-
AppLauncher, Color, Data, Env, Lens, LocalizedString, TextAlignment, UpdateCtx, Widget,
20-
WidgetExt, WindowDesc,
22+
AppLauncher, Color, Data, FontFamily, FontStyle, FontWeight, Lens, LocalizedString,
23+
TextAlignment, Widget, WidgetExt, WindowDesc,
2124
};
2225

2326
const WINDOW_TITLE: LocalizedString<AppState> = LocalizedString::new("Text Options");
@@ -29,18 +32,34 @@ const SPACER_SIZE: f64 = 8.0;
2932

3033
#[derive(Clone, Data, Lens)]
3134
struct AppState {
35+
text: RichText,
3236
line_break_mode: LineBreaking,
3337
alignment: TextAlignment,
3438
}
3539

36-
/// A controller that sets properties on a label.
40+
//NOTE: we implement these traits for our base data (instead of just lensing
41+
//into the RichText object, for the label) so that our label controller can
42+
//have access to the other fields.
43+
impl PietTextStorage for AppState {
44+
fn as_str(&self) -> &str {
45+
self.text.as_str()
46+
}
47+
}
48+
49+
impl TextStorage for AppState {
50+
fn add_attributes(&self, builder: PietTextLayoutBuilder, env: &Env) -> PietTextLayoutBuilder {
51+
self.text.add_attributes(builder, env)
52+
}
53+
}
54+
55+
/// A controller that updates label properties as required.
3756
struct LabelController;
3857

39-
impl Controller<AppState, Label<AppState>> for LabelController {
58+
impl Controller<AppState, RawLabel<AppState>> for LabelController {
4059
#[allow(clippy::float_cmp)]
4160
fn update(
4261
&mut self,
43-
child: &mut Label<AppState>,
62+
child: &mut RawLabel<AppState>,
4463
ctx: &mut UpdateCtx,
4564
old_data: &AppState,
4665
data: &AppState,
@@ -52,6 +71,7 @@ impl Controller<AppState, Label<AppState>> for LabelController {
5271
}
5372
if old_data.alignment != data.alignment {
5473
child.set_text_alignment(data.alignment);
74+
ctx.request_layout();
5575
}
5676
child.update(ctx, old_data, data, env);
5777
}
@@ -63,10 +83,19 @@ pub fn main() {
6383
.title(WINDOW_TITLE)
6484
.window_size((400.0, 600.0));
6585

86+
let text = RichText::new(TEXT.into())
87+
.with_attribute(0..9, Attribute::text_color(Color::rgb(1.0, 0.2, 0.1)))
88+
.with_attribute(0..9, Attribute::size(24.0))
89+
.with_attribute(0..9, Attribute::font_family(FontFamily::SERIF))
90+
.with_attribute(194..239, Attribute::weight(FontWeight::BOLD))
91+
.with_attribute(764.., Attribute::size(12.0))
92+
.with_attribute(764.., Attribute::style(FontStyle::Italic));
93+
6694
// create the initial app state
6795
let initial_state = AppState {
6896
line_break_mode: LineBreaking::Clip,
6997
alignment: Default::default(),
98+
text,
7099
};
71100

72101
// start the application
@@ -78,7 +107,7 @@ pub fn main() {
78107

79108
fn build_root_widget() -> impl Widget<AppState> {
80109
let label = Scroll::new(
81-
Label::new(TEXT)
110+
RawLabel::new()
82111
.with_text_color(Color::BLACK)
83112
.controller(LabelController)
84113
.background(Color::WHITE)

druid/src/core.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ use crate::contexts::ContextState;
2121
use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size, Vec2};
2222
use crate::util::ExtendDrain;
2323
use crate::{
24-
BoxConstraints, Color, Command, Data, Env, Event, EventCtx, InternalEvent, InternalLifeCycle,
25-
LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, RenderContext, Target, TextLayout,
26-
TimerToken, UpdateCtx, Widget, WidgetId,
24+
ArcStr, BoxConstraints, Color, Command, Data, Env, Event, EventCtx, InternalEvent,
25+
InternalLifeCycle, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, RenderContext, Target,
26+
TextLayout, TimerToken, UpdateCtx, Widget, WidgetId,
2727
};
2828

2929
/// Our queue type
@@ -50,7 +50,7 @@ pub struct WidgetPod<T, W> {
5050
env: Option<Env>,
5151
inner: W,
5252
// stashed layout so we don't recompute this when debugging
53-
debug_widget_text: TextLayout,
53+
debug_widget_text: TextLayout<ArcStr>,
5454
}
5555

5656
/// Generic state for all widgets in the hierarchy.
@@ -144,7 +144,7 @@ impl<T, W: Widget<T>> WidgetPod<T, W> {
144144
old_data: None,
145145
env: None,
146146
inner,
147-
debug_widget_text: TextLayout::new(""),
147+
debug_widget_text: TextLayout::new(),
148148
}
149149
}
150150

@@ -433,7 +433,7 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
433433
Color::BLACK
434434
};
435435
let id_string = id.to_raw().to_string();
436-
self.debug_widget_text.set_text(id_string);
436+
self.debug_widget_text.set_text(id_string.into());
437437
self.debug_widget_text.set_text_size(10.0);
438438
self.debug_widget_text.set_text_color(text_color);
439439
self.debug_widget_text.rebuild_if_needed(ctx.text(), env);

druid/src/data.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,6 @@ pub trait Data: Clone + 'static {
119119
//// ANCHOR_END: same_fn
120120
}
121121

122-
/// A reference counted string slice.
123-
///
124-
/// This is a data-friendly way to represent strings in druid. Unlike `String`
125-
/// it cannot be mutated, but unlike `String` it can be cheaply cloned.
126-
pub type ArcStr = Arc<str>;
127-
128122
/// An impl of `Data` suitable for simple types.
129123
///
130124
/// The `same` method is implemented with equality, so the type should

druid/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,15 +189,15 @@ pub use app_delegate::{AppDelegate, DelegateCtx};
189189
pub use box_constraints::BoxConstraints;
190190
pub use command::{sys as commands, Command, Selector, SingleUse, Target};
191191
pub use contexts::{EventCtx, LayoutCtx, LifeCycleCtx, PaintCtx, UpdateCtx};
192-
pub use data::{ArcStr, Data};
192+
pub use data::Data;
193193
pub use env::{Env, Key, KeyOrValue, Value, ValueType};
194194
pub use event::{Event, InternalEvent, InternalLifeCycle, LifeCycle};
195195
pub use ext_event::{ExtEventError, ExtEventSink};
196196
pub use lens::{Lens, LensExt};
197197
pub use localization::LocalizedString;
198198
pub use menu::{sys as platform_menus, ContextMenu, MenuDesc, MenuItem};
199199
pub use mouse::MouseEvent;
200-
pub use text::{FontDescriptor, TextLayout};
200+
pub use text::{ArcStr, FontDescriptor, TextLayout};
201201
pub use widget::{Widget, WidgetExt, WidgetId};
202202
pub use win_handler::DruidHandler;
203203
pub use window::{Window, WindowId};

druid/src/text/attribute.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,11 @@ impl AttributeSpans {
132132
.iter()
133133
.map(|s| (s.range.clone(), PietAttr::Weight(s.attr))),
134134
);
135-
items.extend(self.fg_color.iter().map(|s| {
136-
(
137-
s.range.clone(),
138-
PietAttr::TextColor(s.attr.resolve(env)),
139-
)
140-
}));
135+
items.extend(
136+
self.fg_color
137+
.iter()
138+
.map(|s| (s.range.clone(), PietAttr::TextColor(s.attr.resolve(env)))),
139+
);
141140
items.extend(
142141
self.style
143142
.iter()
@@ -214,6 +213,9 @@ impl<T: Clone> SpanSet<T> {
214213
/// `new_len` is the length of the inserted text.
215214
//TODO: we could be smarter here about just extending the existing spans
216215
//as requred for insertions in the interior of a span.
216+
//TODO: this isn't currently used; it should be used if we use spans with
217+
//some editable type.
218+
#[allow(dead_code)]
217219
fn edit(&mut self, changed: Range<usize>, new_len: usize) {
218220
let old_len = changed.len();
219221
let mut to_insert = None;

druid/src/text/layout.rs

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
1717
use std::ops::Range;
1818

19+
use super::TextStorage;
1920
use crate::kurbo::{Line, Point, Rect, Size};
2021
use crate::piet::{
2122
Color, PietText, PietTextLayout, Text as _, TextAlignment, TextAttribute, TextLayout as _,
2223
TextLayoutBuilder as _,
2324
};
24-
use crate::{ArcStr, Env, FontDescriptor, KeyOrValue, PaintCtx, RenderContext, UpdateCtx};
25+
use crate::{Env, FontDescriptor, KeyOrValue, PaintCtx, RenderContext, UpdateCtx};
2526

2627
/// A component for displaying text on screen.
2728
///
@@ -43,8 +44,8 @@ use crate::{ArcStr, Env, FontDescriptor, KeyOrValue, PaintCtx, RenderContext, Up
4344
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
4445
/// [`Env`]: struct.Env.html
4546
#[derive(Clone)]
46-
pub struct TextLayout {
47-
text: ArcStr,
47+
pub struct TextLayout<T> {
48+
text: Option<T>,
4849
font: KeyOrValue<FontDescriptor>,
4950
// when set, this will be used to override the size in he font descriptor.
5051
// This provides an easy way to change only the font size, while still
@@ -56,16 +57,15 @@ pub struct TextLayout {
5657
alignment: TextAlignment,
5758
}
5859

59-
impl TextLayout {
60+
impl<T: TextStorage> TextLayout<T> {
6061
/// Create a new `TextLayout` object.
6162
///
62-
/// You do not provide the actual text at creation time; instead you pass
63-
/// it in when calling [`rebuild_if_needed`].
63+
/// You must set the text ([`set_text`]) before using this object.
6464
///
65-
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
66-
pub fn new(text: impl Into<ArcStr>) -> Self {
65+
/// [`set_text`]: #method.set_text
66+
pub fn new() -> Self {
6767
TextLayout {
68-
text: text.into(),
68+
text: None,
6969
font: crate::theme::UI_FONT.into(),
7070
text_color: crate::theme::LABEL_COLOR.into(),
7171
text_size_override: None,
@@ -75,6 +75,16 @@ impl TextLayout {
7575
}
7676
}
7777

78+
/// Create a new `TextLayout` with the provided text.
79+
///
80+
/// This is useful when the text is not died to application data.
81+
pub fn with_text(text: impl Into<T>) -> Self {
82+
TextLayout {
83+
text: Some(text.into()),
84+
..TextLayout::new()
85+
}
86+
}
87+
7888
/// Returns `true` if this layout needs to be rebuilt.
7989
///
8090
/// This happens (for instance) after style attributes are modified.
@@ -86,10 +96,9 @@ impl TextLayout {
8696
}
8797

8898
/// Set the text to display.
89-
pub fn set_text(&mut self, text: impl Into<ArcStr>) {
90-
let text = text.into();
91-
if text != self.text {
92-
self.text = text;
99+
pub fn set_text(&mut self, text: T) {
100+
if self.text.is_none() || self.text.as_ref().unwrap().as_str() != text.as_str() {
101+
self.text = Some(text);
93102
self.layout = None;
94103
}
95104
}
@@ -253,29 +262,29 @@ impl TextLayout {
253262
///
254263
/// [`layout`]: trait.Widget.html#method.layout
255264
pub fn rebuild_if_needed(&mut self, factory: &mut PietText, env: &Env) {
256-
if self.layout.is_none() {
257-
let font = self.font.resolve(env);
258-
let color = self.text_color.resolve(env);
259-
let size_override = self.text_size_override.as_ref().map(|key| key.resolve(env));
265+
if let Some(text) = &self.text {
266+
if self.layout.is_none() {
267+
let font = self.font.resolve(env);
268+
let color = self.text_color.resolve(env);
269+
let size_override = self.text_size_override.as_ref().map(|key| key.resolve(env));
260270

261-
let descriptor = if let Some(size) = size_override {
262-
font.with_size(size)
263-
} else {
264-
font
265-
};
271+
let descriptor = if let Some(size) = size_override {
272+
font.with_size(size)
273+
} else {
274+
font
275+
};
266276

267-
self.layout = Some(
268-
factory
269-
.new_text_layout(self.text.clone())
277+
let builder = factory
278+
.new_text_layout(text.clone())
270279
.max_width(self.wrap_width)
271280
.alignment(self.alignment)
272281
.font(descriptor.family.clone(), descriptor.size)
273282
.default_attribute(descriptor.weight)
274283
.default_attribute(descriptor.style)
275-
.default_attribute(TextAttribute::TextColor(color))
276-
.build()
277-
.unwrap(),
278-
)
284+
.default_attribute(TextAttribute::TextColor(color));
285+
let layout = text.add_attributes(builder, env).build().unwrap();
286+
self.layout = Some(layout);
287+
}
279288
}
280289
}
281290

@@ -291,15 +300,18 @@ impl TextLayout {
291300
debug_assert!(
292301
self.layout.is_some(),
293302
"TextLayout::draw called without rebuilding layout object. Text was '{}'",
294-
&self.text
303+
self.text
304+
.as_ref()
305+
.map(|t| t.as_str())
306+
.unwrap_or("layout is missing text")
295307
);
296308
if let Some(layout) = self.layout.as_ref() {
297309
ctx.draw_text(layout, point);
298310
}
299311
}
300312
}
301313

302-
impl std::fmt::Debug for TextLayout {
314+
impl<T> std::fmt::Debug for TextLayout<T> {
303315
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
304316
f.debug_struct("TextLayout")
305317
.field("font", &self.font)
@@ -316,3 +328,9 @@ impl std::fmt::Debug for TextLayout {
316328
.finish()
317329
}
318330
}
331+
332+
impl<T: TextStorage> Default for TextLayout<T> {
333+
fn default() -> Self {
334+
Self::new()
335+
}
336+
}

druid/src/text/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ mod font_descriptor;
2121
mod layout;
2222
pub mod movement;
2323
pub mod selection;
24+
mod storage;
2425
mod text_input;
2526

2627
pub use self::attribute::{Attribute, AttributeSpans};
@@ -31,3 +32,4 @@ pub use self::layout::TextLayout;
3132
pub use self::movement::{movement, Movement};
3233
pub use self::selection::Selection;
3334
pub use self::text_input::{BasicTextInput, EditAction, MouseAction, TextInput};
35+
pub use storage::{ArcStr, RichText, TextStorage};

0 commit comments

Comments
 (0)