diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f4ed7b5b6..28e7a72a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,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 @@ -424,9 +425,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 diff --git a/druid/examples/text.rs b/druid/examples/text.rs new file mode 100644 index 0000000000..46d051f757 --- /dev/null +++ b/druid/examples/text.rs @@ -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 = 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> for LabelController { + #[allow(clippy::float_cmp)] + fn update( + &mut self, + child: &mut Label, + 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 { + 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) +} diff --git a/druid/examples/web/src/lib.rs b/druid/examples/web/src/lib.rs index 78fc69f620..862dad51ac 100644 --- a/druid/examples/web/src/lib.rs +++ b/druid/examples/web/src/lib.rs @@ -79,3 +79,4 @@ impl_example!(switches); impl_example!(timer); impl_example!(view_switcher); impl_example!(widget_gallery); +impl_example!(text); diff --git a/druid/src/text/layout.rs b/druid/src/text/layout.rs index 2ed0f15fc0..abe2d5abb4 100644 --- a/druid/src/text/layout.rs +++ b/druid/src/text/layout.rs @@ -54,6 +54,7 @@ pub struct TextLayout { cached_text_size: Option, // the underlying layout object. This is constructed lazily. layout: Option, + wrap_width: f64, } impl TextLayout { @@ -73,6 +74,7 @@ impl TextLayout { text_size_override: None, cached_text_size: None, layout: None, + wrap_width: f64::INFINITY, } } @@ -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. @@ -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) diff --git a/druid/src/widget/label.rs b/druid/src/widget/label.rs index 8b5d49810e..71cc8f996c 100644 --- a/druid/src/widget/label.rs +++ b/druid/src/widget/label.rs @@ -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. @@ -51,11 +51,23 @@ pub struct Dynamic { pub struct Label { text: LabelText, 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 Label { /// Construct a new `Label` widget. /// @@ -79,6 +91,7 @@ impl Label { Self { text, layout, + line_break_mode: LineBreaking::Clip, needs_rebuild: true, } } @@ -141,7 +154,20 @@ impl Label { 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>) { self.text = text.into(); self.needs_rebuild = true; @@ -156,6 +182,10 @@ impl Label { /// /// The argument can be either a `Color` or a [`Key`]. /// + /// 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`]: ../struct.Key.html pub fn set_text_color(&mut self, color: impl Into>) { self.layout.set_text_color(color); @@ -166,6 +196,10 @@ impl Label { /// /// The argument can be either an `f64` or a [`Key`]. /// + /// 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`]: ../struct.Key.html pub fn set_text_size(&mut self, size: impl Into>) { self.layout.set_text_size(size); @@ -177,6 +211,10 @@ impl Label { /// The argument can be a [`FontDescriptor`] or a [`Key`] /// 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`]: ../struct.Key.html @@ -185,6 +223,17 @@ impl Label { 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); @@ -251,18 +300,27 @@ impl Widget for Label { 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) } } diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 5af57a32f0..345af60e3a 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -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};