Skip to content

Commit

Permalink
Expose wrap width in TextLayout, add LineBreaking options
Browse files Browse the repository at this point in the history
With this patch, the TextLayout object can now be given a width
for wrapping words. This is exposed in the Label via a new
LineBreaking enum, with three options. With LineBreaking::WordWrap,
the TextLayout's wrap-width is set to the maximum width of the label's
BoxConstraints, and lines are broken appropriately. The other two
options (LineBreaking::Overflow and LineBreaking::Clip) both disable
line-breaking; in the former case if text exceeds the width of
the label it is painted outside of the label's bounds, and in the
latter case the text is clipped to the bounds of the label.

It would be nice if, in the clipping case, we could use a gradient
mask or something to fade the edges of the clipped text, but I don't
believe this is currently possible in piet. A simple alternative
of drawing a gradient from (transparent -> label background color)
doesn't work, because label's do not have an explicit background color,
which I think is also correct.

progress on #1192
  • Loading branch information
cmyr committed Sep 11, 2020
1 parent 30922d0 commit bfe33a9
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 10 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ You can find its changes [documented below](#060---2020-06-01).
- `Movement::StartOfDocument`, `Movement::EndOfDocument`. ([#1092] by [@sysint64])
- `TextLayout` type simplifies drawing text ([#1182] by [@cmyr])
- Implementation of `Data` trait for `i128` and `u128` primitive data types. ([#1214] by [@koutoftimer])
- `LineBreaking` enum allows configuration of label line-breaking ([#1195] by [@cmyr])

### Changed

Expand Down Expand Up @@ -435,9 +436,10 @@ Last release without a changelog :(
[#1171]: https://github.com/linebender/druid/pull/1171
[#1172]: https://github.com/linebender/druid/pull/1172
[#1173]: https://github.com/linebender/druid/pull/1173
[#1182]: https://github.com/linebender/druid/pull/1185
[#1182]: https://github.com/linebender/druid/pull/1182
[#1185]: https://github.com/linebender/druid/pull/1185
[#1092]: https://github.com/linebender/druid/pull/1092
[#1195]: https://github.com/linebender/druid/pull/1195
[#1204]: https://github.com/linebender/druid/pull/1204
[#1205]: https://github.com/linebender/druid/pull/1205
[#1214]: https://github.com/linebender/druid/pull/1214
Expand Down
101 changes: 101 additions & 0 deletions druid/examples/text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2020 The Druid 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.

//! An example of various text layout features.
use druid::widget::{Controller, Flex, Label, LineBreaking, RadioGroup, Scroll};
use druid::{
AppLauncher, Color, Data, Env, Lens, LocalizedString, UpdateCtx, Widget, WidgetExt, WindowDesc,
};

const WINDOW_TITLE: LocalizedString<AppState> = LocalizedString::new("Text Options");

const TEXT: &str = r#"Contrary to what we would like to believe, there is no such thing as a structureless group. Any group of people of whatever nature that comes together for any length of time for any purpose will inevitably structure itself in some fashion. The structure may be flexible; it may vary over time; it may evenly or unevenly distribute tasks, power and resources over the members of the group. But it will be formed regardless of the abilities, personalities, or intentions of the people involved. The very fact that we are individuals, with different talents, predispositions, and backgrounds makes this inevitable. Only if we refused to relate or interact on any basis whatsoever could we approximate structurelessness -- and that is not the nature of a human group.
This means that to strive for a structureless group is as useful, and as deceptive, as to aim at an "objective" news story, "value-free" social science, or a "free" economy. A "laissez faire" group is about as realistic as a "laissez faire" society; the idea becomes a smokescreen for the strong or the lucky to establish unquestioned hegemony over others. This hegemony can be so easily established because the idea of "structurelessness" does not prevent the formation of informal structures, only formal ones. Similarly "laissez faire" philosophy did not prevent the economically powerful from establishing control over wages, prices, and distribution of goods; it only prevented the government from doing so. Thus structurelessness becomes a way of masking power, and within the women's movement is usually most strongly advocated by those who are the most powerful (whether they are conscious of their power or not). As long as the structure of the group is informal, the rules of how decisions are made are known only to a few and awareness of power is limited to those who know the rules. Those who do not know the rules and are not chosen for initiation must remain in confusion, or suffer from paranoid delusions that something is happening of which they are not quite aware."#;

const SPACER_SIZE: f64 = 8.0;

#[derive(Clone, Data, Lens)]
struct AppState {
/// the width at which to wrap lines.
line_break_mode: LineBreaking,
}

/// A controller that sets properties on a label.
struct LabelController;

impl Controller<AppState, Label<AppState>> for LabelController {
#[allow(clippy::float_cmp)]
fn update(
&mut self,
child: &mut Label<AppState>,
ctx: &mut UpdateCtx,
old_data: &AppState,
data: &AppState,
env: &Env,
) {
if old_data.line_break_mode != data.line_break_mode {
child.set_line_break_mode(data.line_break_mode);
ctx.request_layout();
}
child.update(ctx, old_data, data, env);
}
}

pub fn main() {
// describe the main window
let main_window = WindowDesc::new(build_root_widget)
.title(WINDOW_TITLE)
.window_size((400.0, 600.0));

// create the initial app state
let initial_state = AppState {
line_break_mode: LineBreaking::Clip,
};

// start the application
AppLauncher::with_window(main_window)
.use_simple_logger()
.launch(initial_state)
.expect("Failed to launch application");
}

fn build_root_widget() -> impl Widget<AppState> {
let label = Scroll::new(
Label::new(TEXT)
.with_text_color(Color::BLACK)
.controller(LabelController)
.background(Color::WHITE)
.expand_width()
.padding((SPACER_SIZE * 4.0, SPACER_SIZE))
.background(Color::grey8(222)),
)
.vertical();

let line_break_chooser = Flex::column()
.with_child(Label::new("Line break mode"))
.with_spacer(SPACER_SIZE)
.with_child(RadioGroup::new(vec![
("Clip", LineBreaking::Clip),
("Wrap", LineBreaking::WordWrap),
("Overflow", LineBreaking::Overflow),
]))
.lens(AppState::line_break_mode);

Flex::column()
.with_spacer(SPACER_SIZE)
.with_child(line_break_chooser)
.with_spacer(SPACER_SIZE)
.with_flex_child(label, 1.0)
}
1 change: 1 addition & 0 deletions druid/examples/web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,4 @@ impl_example!(switches);
impl_example!(timer);
impl_example!(view_switcher);
impl_example!(widget_gallery);
impl_example!(text);
14 changes: 14 additions & 0 deletions druid/src/text/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ pub struct TextLayout {
cached_text_size: Option<f64>,
// the underlying layout object. This is constructed lazily.
layout: Option<PietTextLayout>,
wrap_width: f64,
}

impl TextLayout {
Expand All @@ -73,6 +74,7 @@ impl TextLayout {
text_size_override: None,
cached_text_size: None,
layout: None,
wrap_width: f64::INFINITY,
}
}

Expand Down Expand Up @@ -123,6 +125,17 @@ impl TextLayout {
self.layout = None;
}

/// Set the width at which to wrap words.
///
/// You may pass `f64::INFINITY` to disable word wrapping
/// (the default behaviour).
pub fn set_wrap_width(&mut self, width: f64) {
self.wrap_width = width;
if let Some(layout) = self.layout.as_mut() {
let _ = layout.update_width(width);
}
}

/// The size of the laid-out text.
///
/// This is not meaningful until [`rebuild_if_needed`] has been called.
Expand Down Expand Up @@ -225,6 +238,7 @@ impl TextLayout {
self.layout = Some(
factory
.new_text_layout(self.text.clone())
.max_width(self.wrap_width)
.font(descriptor.family.clone(), descriptor.size)
.default_attribute(descriptor.weight)
.default_attribute(descriptor.style)
Expand Down
74 changes: 66 additions & 8 deletions druid/src/widget/label.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
//! A label widget.
use crate::piet::{Color, PietText};
use crate::widget::prelude::*;
use crate::{
BoxConstraints, Data, Env, Event, EventCtx, FontDescriptor, KeyOrValue, LayoutCtx, LifeCycle,
LifeCycleCtx, LocalizedString, PaintCtx, Point, Size, TextLayout, UpdateCtx, Widget,
BoxConstraints, Data, FontDescriptor, KeyOrValue, LocalizedString, Point, Size, TextLayout,
};

// added padding between the edges of the widget and the text.
Expand Down Expand Up @@ -51,11 +51,23 @@ pub struct Dynamic<T> {
pub struct Label<T> {
text: LabelText<T>,
layout: TextLayout,
line_break_mode: LineBreaking,
// if our text is manually changed we need to rebuild the layout
// before using it again.
needs_rebuild: bool,
}

/// Options for handling lines that are too wide for the label.
#[derive(Debug, Clone, Copy, PartialEq, Data)]
pub enum LineBreaking {
/// Lines are broken at word boundaries.
WordWrap,
/// Lines are truncated to the width of the label.
Clip,
/// Lines overflow the label.
Overflow,
}

impl<T: Data> Label<T> {
/// Construct a new `Label` widget.
///
Expand All @@ -79,6 +91,7 @@ impl<T: Data> Label<T> {
Self {
text,
layout,
line_break_mode: LineBreaking::Clip,
needs_rebuild: true,
}
}
Expand Down Expand Up @@ -141,7 +154,20 @@ impl<T: Data> Label<T> {
self
}

/// Builder-style method to set the [`LineBreaking`] behaviour.
///
/// [`LineBreaking`]: enum.LineBreaking.html
pub fn with_line_break_mode(mut self, mode: LineBreaking) -> Self {
self.set_line_break_mode(mode);
self
}

/// Set the label's text.
///
/// If you change this property, you are responsible for calling
/// [`request_layout`] to ensure the label is updated.
///
/// [`request_layout`]: ../struct.EventCtx.html#method.request_layout
pub fn set_text(&mut self, text: impl Into<LabelText<T>>) {
self.text = text.into();
self.needs_rebuild = true;
Expand All @@ -156,6 +182,10 @@ impl<T: Data> Label<T> {
///
/// The argument can be either a `Color` or a [`Key<Color>`].
///
/// If you change this property, you are responsible for calling
/// [`request_layout`] to ensure the label is updated.
///
/// [`request_layout`]: ../struct.EventCtx.html#method.request_layout
/// [`Key<Color>`]: ../struct.Key.html
pub fn set_text_color(&mut self, color: impl Into<KeyOrValue<Color>>) {
self.layout.set_text_color(color);
Expand All @@ -166,6 +196,10 @@ impl<T: Data> Label<T> {
///
/// The argument can be either an `f64` or a [`Key<f64>`].
///
/// If you change this property, you are responsible for calling
/// [`request_layout`] to ensure the label is updated.
///
/// [`request_layout`]: ../struct.EventCtx.html#method.request_layout
/// [`Key<f64>`]: ../struct.Key.html
pub fn set_text_size(&mut self, size: impl Into<KeyOrValue<f64>>) {
self.layout.set_text_size(size);
Expand All @@ -177,6 +211,10 @@ impl<T: Data> Label<T> {
/// The argument can be a [`FontDescriptor`] or a [`Key<FontDescriptor>`]
/// that refers to a font defined in the [`Env`].
///
/// If you change this property, you are responsible for calling
/// [`request_layout`] to ensure the label is updated.
///
/// [`request_layout`]: ../struct.EventCtx.html#method.request_layout
/// [`Env`]: ../struct.Env.html
/// [`FontDescriptor`]: ../struct.FontDescriptor.html
/// [`Key<FontDescriptor>`]: ../struct.Key.html
Expand All @@ -185,6 +223,17 @@ impl<T: Data> Label<T> {
self.needs_rebuild = true;
}

/// Set the [`LineBreaking`] behaviour.
///
/// If you change this property, you are responsible for calling
/// [`request_layout`] to ensure the label is updated.
///
/// [`request_layout`]: ../struct.EventCtx.html#method.request_layout
/// [`LineBreaking`]: enum.LineBreaking.html
pub fn set_line_break_mode(&mut self, mode: LineBreaking) {
self.line_break_mode = mode;
}

fn rebuild_if_needed(&mut self, factory: &mut PietText, data: &T, env: &Env) {
if self.needs_rebuild {
self.text.resolve(data, env);
Expand Down Expand Up @@ -251,18 +300,27 @@ impl<T: Data> Widget<T> for Label<T> {

fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
bc.debug_check("Label");

let width = match self.line_break_mode {
LineBreaking::WordWrap => bc.max().width - LABEL_X_PADDING * 2.0,
_ => f64::INFINITY,
};

self.rebuild_if_needed(&mut ctx.text(), data, env);
self.layout.set_wrap_width(width);

let text_size = self.layout.size();
bc.constrain(Size::new(
text_size.width + 2. * LABEL_X_PADDING,
text_size.height,
))
let mut text_size = self.layout.size();
text_size.width += 2. * LABEL_X_PADDING;
bc.constrain(text_size)
}

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);
let label_size = ctx.size();

if self.line_break_mode == LineBreaking::Clip {
ctx.clip(label_size.to_rect());
}
self.layout.draw(ctx, origin)
}
}
Expand Down
2 changes: 1 addition & 1 deletion druid/src/widget/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pub use either::Either;
pub use env_scope::EnvScope;
pub use flex::{CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment};
pub use identity_wrapper::IdentityWrapper;
pub use label::{Label, LabelText};
pub use label::{Label, LabelText, LineBreaking};
pub use list::{List, ListIter};
pub use padding::Padding;
pub use painter::{BackgroundBrush, Painter};
Expand Down

0 comments on commit bfe33a9

Please sign in to comment.