Skip to content

Commit

Permalink
Basic rich text support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cmyr committed Sep 29, 2020
1 parent c87350d commit 49466e1
Show file tree
Hide file tree
Showing 13 changed files with 250 additions and 103 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ You can find its changes [documented below](#060---2020-06-01).
- CONFIGURE_WINDOW command to allow reconfiguration of an existing window. ([#1235] by [@rjwittams])
- `RawLabel` widget displays text `Data`. ([#1252] by [@cmyr])
- 'Tabs' widget allowing static and dynamic tabbed layouts. ([#1160] by [@rjwittams])
- `RichText` and `Attribute` types for creating rich text ([#1255] by [@cmyr])

### Changed

Expand Down Expand Up @@ -476,6 +477,7 @@ Last release without a changelog :(
[#1245]: https://github.com/linebender/druid/pull/1245
[#1251]: https://github.com/linebender/druid/pull/1251
[#1252]: https://github.com/linebender/druid/pull/1252
[#1255]: https://github.com/linebender/druid/pull/1255

[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
4 changes: 2 additions & 2 deletions druid/examples/custom_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use druid::kurbo::BezPath;
use druid::piet::{FontFamily, ImageFormat, InterpolationMode};
use druid::widget::prelude::*;
use druid::{
Affine, AppLauncher, Color, FontDescriptor, LocalizedString, Point, Rect, TextLayout,
Affine, AppLauncher, ArcStr, Color, FontDescriptor, LocalizedString, Point, Rect, TextLayout,
WindowDesc,
};

Expand Down Expand Up @@ -86,7 +86,7 @@ impl Widget<String> for CustomWidget {

// Text is easy; in real use TextLayout should be stored in the widget
// and reused.
let mut layout = TextLayout::new(data.as_str());
let mut layout = TextLayout::<ArcStr>::from_text(data.to_owned());
layout.set_font(FontDescriptor::new(FontFamily::SERIF).with_size(24.0));
layout.set_text_color(fill_color);
layout.rebuild_if_needed(ctx.text(), env);
Expand Down
43 changes: 36 additions & 7 deletions druid/examples/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@

//! An example of various text layout features.
use druid::widget::{Controller, Flex, Label, LineBreaking, RadioGroup, Scroll};
use druid::piet::{PietTextLayoutBuilder, TextStorage as PietTextStorage};
use druid::text::{Attribute, RichText, TextStorage};
use druid::widget::prelude::*;
use druid::widget::{Controller, Flex, Label, LineBreaking, RadioGroup, RawLabel, Scroll};
use druid::{
AppLauncher, Color, Data, Env, Lens, LocalizedString, TextAlignment, UpdateCtx, Widget,
WidgetExt, WindowDesc,
AppLauncher, Color, Data, FontFamily, FontStyle, FontWeight, Lens, LocalizedString,
TextAlignment, Widget, WidgetExt, WindowDesc,
};

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

#[derive(Clone, Data, Lens)]
struct AppState {
text: RichText,
line_break_mode: LineBreaking,
alignment: TextAlignment,
}

/// A controller that sets properties on a label.
//NOTE: we implement these traits for our base data (instead of just lensing
//into the RichText object, for the label) so that our label controller can
//have access to the other fields.
impl PietTextStorage for AppState {
fn as_str(&self) -> &str {
self.text.as_str()
}
}

impl TextStorage for AppState {
fn add_attributes(&self, builder: PietTextLayoutBuilder, env: &Env) -> PietTextLayoutBuilder {
self.text.add_attributes(builder, env)
}
}

/// A controller that updates label properties as required.
struct LabelController;

impl Controller<AppState, Label<AppState>> for LabelController {
impl Controller<AppState, RawLabel<AppState>> for LabelController {
#[allow(clippy::float_cmp)]
fn update(
&mut self,
child: &mut Label<AppState>,
child: &mut RawLabel<AppState>,
ctx: &mut UpdateCtx,
old_data: &AppState,
data: &AppState,
Expand All @@ -52,6 +71,7 @@ impl Controller<AppState, Label<AppState>> for LabelController {
}
if old_data.alignment != data.alignment {
child.set_text_alignment(data.alignment);
ctx.request_layout();
}
child.update(ctx, old_data, data, env);
}
Expand All @@ -63,10 +83,19 @@ pub fn main() {
.title(WINDOW_TITLE)
.window_size((400.0, 600.0));

let text = RichText::new(TEXT.into())
.with_attribute(0..9, Attribute::text_color(Color::rgb(1.0, 0.2, 0.1)))
.with_attribute(0..9, Attribute::size(24.0))
.with_attribute(0..9, Attribute::font_family(FontFamily::SERIF))
.with_attribute(194..239, Attribute::weight(FontWeight::BOLD))
.with_attribute(764.., Attribute::size(12.0))
.with_attribute(764.., Attribute::style(FontStyle::Italic));

// create the initial app state
let initial_state = AppState {
line_break_mode: LineBreaking::Clip,
alignment: Default::default(),
text,
};

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

fn build_root_widget() -> impl Widget<AppState> {
let label = Scroll::new(
Label::new(TEXT)
RawLabel::new()
.with_text_color(Color::BLACK)
.controller(LabelController)
.background(Color::WHITE)
Expand Down
12 changes: 6 additions & 6 deletions druid/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ use crate::contexts::ContextState;
use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size, Vec2};
use crate::util::ExtendDrain;
use crate::{
BoxConstraints, Color, Command, Data, Env, Event, EventCtx, InternalEvent, InternalLifeCycle,
LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, RenderContext, Target, TextLayout,
TimerToken, UpdateCtx, Widget, WidgetId,
ArcStr, BoxConstraints, Color, Command, Data, Env, Event, EventCtx, InternalEvent,
InternalLifeCycle, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, RenderContext, Target,
TextLayout, TimerToken, UpdateCtx, Widget, WidgetId,
};

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

/// Generic state for all widgets in the hierarchy.
Expand Down Expand Up @@ -144,7 +144,7 @@ impl<T, W: Widget<T>> WidgetPod<T, W> {
old_data: None,
env: None,
inner,
debug_widget_text: TextLayout::new(""),
debug_widget_text: TextLayout::new(),
}
}

Expand Down Expand Up @@ -433,7 +433,7 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
Color::BLACK
};
let id_string = id.to_raw().to_string();
self.debug_widget_text.set_text(id_string);
self.debug_widget_text.set_text(id_string.into());
self.debug_widget_text.set_text_size(10.0);
self.debug_widget_text.set_text_color(text_color);
self.debug_widget_text.rebuild_if_needed(ctx.text(), env);
Expand Down
6 changes: 0 additions & 6 deletions druid/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,6 @@ pub trait Data: Clone + 'static {
//// ANCHOR_END: same_fn
}

/// A reference counted string slice.
///
/// This is a data-friendly way to represent strings in druid. Unlike `String`
/// it cannot be mutated, but unlike `String` it can be cheaply cloned.
pub type ArcStr = Arc<str>;

/// An impl of `Data` suitable for simple types.
///
/// The `same` method is implemented with equality, so the type should
Expand Down
4 changes: 2 additions & 2 deletions druid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,15 +191,15 @@ pub use app_delegate::{AppDelegate, DelegateCtx};
pub use box_constraints::BoxConstraints;
pub use command::{sys as commands, Command, Selector, SingleUse, Target};
pub use contexts::{EventCtx, LayoutCtx, LifeCycleCtx, PaintCtx, UpdateCtx};
pub use data::{ArcStr, Data};
pub use data::Data;
pub use env::{Env, Key, KeyOrValue, Value, ValueType};
pub use event::{Event, InternalEvent, InternalLifeCycle, LifeCycle};
pub use ext_event::{ExtEventError, ExtEventSink};
pub use lens::{Lens, LensExt};
pub use localization::LocalizedString;
pub use menu::{sys as platform_menus, ContextMenu, MenuDesc, MenuItem};
pub use mouse::MouseEvent;
pub use text::{FontDescriptor, TextLayout};
pub use text::{ArcStr, FontDescriptor, TextLayout};
pub use widget::{Widget, WidgetExt, WidgetId};
pub use win_handler::DruidHandler;
pub use window::{Window, WindowId};
Expand Down
14 changes: 8 additions & 6 deletions druid/src/text/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,11 @@ impl AttributeSpans {
.iter()
.map(|s| (s.range.clone(), PietAttr::Weight(s.attr))),
);
items.extend(self.fg_color.iter().map(|s| {
(
s.range.clone(),
PietAttr::TextColor(s.attr.resolve(env)),
)
}));
items.extend(
self.fg_color
.iter()
.map(|s| (s.range.clone(), PietAttr::TextColor(s.attr.resolve(env)))),
);
items.extend(
self.style
.iter()
Expand Down Expand Up @@ -214,6 +213,9 @@ impl<T: Clone> SpanSet<T> {
/// `new_len` is the length of the inserted text.
//TODO: we could be smarter here about just extending the existing spans
//as requred for insertions in the interior of a span.
//TODO: this isn't currently used; it should be used if we use spans with
//some editable type.
#[allow(dead_code)]
fn edit(&mut self, changed: Range<usize>, new_len: usize) {
let old_len = changed.len();
let mut to_insert = None;
Expand Down
80 changes: 49 additions & 31 deletions druid/src/text/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
use std::ops::Range;

use super::TextStorage;
use crate::kurbo::{Line, Point, Rect, Size};
use crate::piet::{
Color, PietText, PietTextLayout, Text as _, TextAlignment, TextAttribute, TextLayout as _,
TextLayoutBuilder as _,
};
use crate::{ArcStr, Env, FontDescriptor, KeyOrValue, PaintCtx, RenderContext, UpdateCtx};
use crate::{Env, FontDescriptor, KeyOrValue, PaintCtx, RenderContext, UpdateCtx};

/// A component for displaying text on screen.
///
Expand All @@ -43,8 +44,8 @@ use crate::{ArcStr, Env, FontDescriptor, KeyOrValue, PaintCtx, RenderContext, Up
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
/// [`Env`]: struct.Env.html
#[derive(Clone)]
pub struct TextLayout {
text: ArcStr,
pub struct TextLayout<T> {
text: Option<T>,
font: KeyOrValue<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
Expand All @@ -56,16 +57,15 @@ pub struct TextLayout {
alignment: TextAlignment,
}

impl TextLayout {
impl<T: TextStorage> TextLayout<T> {
/// 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`].
/// You must set the text ([`set_text`]) before using this object.
///
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
pub fn new(text: impl Into<ArcStr>) -> Self {
/// [`set_text`]: #method.set_text
pub fn new() -> Self {
TextLayout {
text: text.into(),
text: None,
font: crate::theme::UI_FONT.into(),
text_color: crate::theme::LABEL_COLOR.into(),
text_size_override: None,
Expand All @@ -75,6 +75,16 @@ impl TextLayout {
}
}

/// Create a new `TextLayout` with the provided text.
///
/// This is useful when the text is not died to application data.
pub fn from_text(text: impl Into<T>) -> Self {
TextLayout {
text: Some(text.into()),
..TextLayout::new()
}
}

/// Returns `true` if this layout needs to be rebuilt.
///
/// This happens (for instance) after style attributes are modified.
Expand All @@ -86,10 +96,9 @@ impl TextLayout {
}

/// Set the text to display.
pub fn set_text(&mut self, text: impl Into<ArcStr>) {
let text = text.into();
if text != self.text {
self.text = text;
pub fn set_text(&mut self, text: T) {
if self.text.is_none() || self.text.as_ref().unwrap().as_str() != text.as_str() {
self.text = Some(text);
self.layout = None;
}
}
Expand Down Expand Up @@ -253,29 +262,29 @@ impl TextLayout {
///
/// [`layout`]: trait.Widget.html#method.layout
pub fn rebuild_if_needed(&mut self, factory: &mut PietText, env: &Env) {
if self.layout.is_none() {
let font = self.font.resolve(env);
let color = self.text_color.resolve(env);
let size_override = self.text_size_override.as_ref().map(|key| key.resolve(env));
if let Some(text) = &self.text {
if self.layout.is_none() {
let font = self.font.resolve(env);
let color = self.text_color.resolve(env);
let size_override = self.text_size_override.as_ref().map(|key| key.resolve(env));

let descriptor = if let Some(size) = size_override {
font.with_size(size)
} else {
font
};
let descriptor = if let Some(size) = size_override {
font.with_size(size)
} else {
font
};

self.layout = Some(
factory
.new_text_layout(self.text.clone())
let builder = factory
.new_text_layout(text.clone())
.max_width(self.wrap_width)
.alignment(self.alignment)
.font(descriptor.family.clone(), descriptor.size)
.default_attribute(descriptor.weight)
.default_attribute(descriptor.style)
.default_attribute(TextAttribute::TextColor(color))
.build()
.unwrap(),
)
.default_attribute(TextAttribute::TextColor(color));
let layout = text.add_attributes(builder, env).build().unwrap();
self.layout = Some(layout);
}
}
}

Expand All @@ -291,15 +300,18 @@ impl TextLayout {
debug_assert!(
self.layout.is_some(),
"TextLayout::draw called without rebuilding layout object. Text was '{}'",
&self.text
self.text
.as_ref()
.map(|t| t.as_str())
.unwrap_or("layout is missing text")
);
if let Some(layout) = self.layout.as_ref() {
ctx.draw_text(layout, point);
}
}
}

impl std::fmt::Debug for TextLayout {
impl<T> std::fmt::Debug for TextLayout<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("TextLayout")
.field("font", &self.font)
Expand All @@ -316,3 +328,9 @@ impl std::fmt::Debug for TextLayout {
.finish()
}
}

impl<T: TextStorage> Default for TextLayout<T> {
fn default() -> Self {
Self::new()
}
}
2 changes: 2 additions & 0 deletions druid/src/text/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod font_descriptor;
mod layout;
pub mod movement;
pub mod selection;
mod storage;
mod text_input;

pub use self::attribute::{Attribute, AttributeSpans};
Expand All @@ -31,3 +32,4 @@ 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};
pub use storage::{ArcStr, RichText, TextStorage};
Loading

0 comments on commit 49466e1

Please sign in to comment.