xilem/masonry/src/widgets/prose.rs

224 lines
7.8 KiB
Rust

// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
#![warn(missing_docs)]
use accesskit::{Node, Role};
use smallvec::{smallvec, SmallVec};
use tracing::{trace_span, Span};
use vello::kurbo::{Point, Rect, Size};
use vello::Scene;
use crate::core::{
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, PaintCtx, PointerEvent, QueryCtx,
RegisterCtx, TextEvent, Update, UpdateCtx, Widget, WidgetId, WidgetMut, WidgetPod,
};
use crate::widgets::{Padding, TextArea};
/// Added padding between each horizontal edge of the widget
/// and the text in logical pixels.
///
/// This gives the text the some slight breathing room.
const PROSE_PADDING: Padding = Padding::horizontal(2.0);
// The bottom padding is to workaround https://github.com/linebender/parley/issues/165
// const PROSE_PADDING: Padding = Padding::new(0.0, 2.0, 5.0, 2.0);
/// The prose widget displays immutable text which can be
/// selected within.
///
/// The text can also be copied from, but cannot be modified by the user.
/// Note that copying is not yet implemented.
///
/// At runtime, most properties of the text will be set using [`text_mut`](Self::text_mut).
/// This is because `Prose` largely serves as a wrapper around a [`TextArea`].
///
/// This should be used instead of [`Label`](super::Label) for immutable text,
/// as it enables users to copy/paste from the text.
///
/// This widget has no actions.
///
#[doc = crate::include_screenshot!("widget/screenshots/masonry__widget__prose__tests__prose_alignment_flex.png", "Multiple lines with different alignments.")]
pub struct Prose {
text: WidgetPod<TextArea<false>>,
/// Whether to clip the contained text.
clip: bool,
}
impl Prose {
/// Create a new `Prose` with the given text.
///
/// To use non-default text properties, use [`from_text_area`](Self::from_text_area) instead.
pub fn new(text: &str) -> Self {
Self::from_text_area(TextArea::new_immutable(text))
}
/// Create a new `Prose` from a styled text area.
pub fn from_text_area(text: TextArea<false>) -> Self {
let text = text.with_padding_if_default(PROSE_PADDING);
Self {
text: WidgetPod::new(text),
clip: false,
}
}
/// Create a new `Prose` from a styled text area in a [`WidgetPod`].
///
/// Note that the default padding used for prose will not be applied.
pub fn from_text_area_pod(text: WidgetPod<TextArea<false>>) -> Self {
Self { text, clip: false }
}
/// Whether to clip the text to the available space.
///
/// If this is set to true, it is recommended, but not required, that this
/// wraps a text area with [word wrapping](TextArea::with_word_wrap) enabled.
///
/// To modify this on active prose, use [`set_clip`](Self::set_clip).
pub fn with_clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
/// Read the underlying text area. Useful for getting its ID.
// This is a bit of a hack, to work around `from_text_area_pod` not being
// able to set padding.
pub fn text_area_pod(&self) -> &WidgetPod<TextArea<false>> {
&self.text
}
}
// --- MARK: WIDGETMUT ---
impl Prose {
/// Edit the underlying text area.
///
/// Used to modify most properties of the text.
pub fn text_mut<'t>(self: &'t mut WidgetMut<'_, Self>) -> WidgetMut<'t, TextArea<false>> {
self.ctx.get_mut(&mut self.widget.text)
}
/// Whether to clip the text to the available space.
///
/// If this is set to true, it is recommended, but not required, that this
/// wraps a text area with [word wrapping](TextArea::set_word_wrap) enabled.
///
/// The runtime requivalent of [`with_clip`](Self::with_clip).
pub fn set_clip(self: &mut WidgetMut<'_, Self>, clip: bool) {
self.widget.clip = clip;
self.ctx.request_layout();
}
}
// --- MARK: IMPL WIDGET ---
impl Widget for Prose {
fn on_pointer_event(&mut self, _: &mut EventCtx, _: &PointerEvent) {}
fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {}
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {}
fn register_children(&mut self, ctx: &mut RegisterCtx) {
ctx.register_child(&mut self.text);
}
fn update(&mut self, _ctx: &mut UpdateCtx, _event: &Update) {}
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
// TODO: Set minimum to deal with alignment
let size = ctx.run_layout(&mut self.text, bc);
ctx.place_child(&mut self.text, Point::ORIGIN);
if self.clip {
// Workaround for https://github.com/linebender/parley/issues/165
let clip_size = Size::new(size.width, size.height + 20.);
ctx.set_clip_path(Rect::from_origin_size(Point::ORIGIN, clip_size));
}
size
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut Scene) {
// All painting is handled by the child
}
fn accessibility_role(&self) -> Role {
Role::GenericContainer
}
fn accessibility(&mut self, _ctx: &mut AccessCtx, _node: &mut Node) {}
fn children_ids(&self) -> SmallVec<[WidgetId; 16]> {
smallvec![self.text.id()]
}
fn make_trace_span(&self, ctx: &QueryCtx<'_>) -> Span {
trace_span!("Prose", id = ctx.widget_id().trace())
}
fn get_debug_text(&self) -> Option<String> {
self.clip.then(|| "(clip)".into())
}
}
// TODO - Add more tests
#[cfg(test)]
mod tests {
use parley::layout::Alignment;
use parley::StyleProperty;
use vello::kurbo::Size;
use super::*;
use crate::assert_render_snapshot;
use crate::testing::TestHarness;
use crate::widgets::{CrossAxisAlignment, Flex, SizedBox, TextArea};
#[test]
/// A wrapping prose's alignment should be respected, regardless of
/// its parent's alignment.
fn prose_clipping() {
let prose = Prose::from_text_area(
TextArea::new_immutable("Hello this text should be truncated")
.with_style(StyleProperty::FontSize(10.0))
.with_word_wrap(false),
)
.with_clip(true);
let sized_box = Flex::row().with_child(SizedBox::new(prose).width(60.));
let mut harness = TestHarness::create_with_size(sized_box, Size::new(80.0, 15.0));
assert_render_snapshot!(harness, "prose_clipping");
}
#[test]
/// A wrapping prose's alignment should be respected, regardless of
/// its parent's alignment.
fn prose_alignment_flex() {
fn base_prose(alignment: Alignment) -> Prose {
// Trailing whitespace is displayed when laying out prose.
Prose::from_text_area(
TextArea::new_immutable("Hello ")
.with_style(StyleProperty::FontSize(10.0))
.with_alignment(alignment)
.with_word_wrap(true),
)
}
let prose1 = base_prose(Alignment::Start);
let prose2 = base_prose(Alignment::Middle);
let prose3 = base_prose(Alignment::End);
let prose4 = base_prose(Alignment::Start);
let prose5 = base_prose(Alignment::Middle);
let prose6 = base_prose(Alignment::End);
let flex = Flex::column()
.with_flex_child(prose1, CrossAxisAlignment::Start)
.with_flex_child(prose2, CrossAxisAlignment::Start)
.with_flex_child(prose3, CrossAxisAlignment::Start)
.with_flex_child(prose4, CrossAxisAlignment::Center)
.with_flex_child(prose5, CrossAxisAlignment::Center)
.with_flex_child(prose6, CrossAxisAlignment::Center)
.gap(0.0);
let mut harness = TestHarness::create_with_size(flex, Size::new(80.0, 80.0));
assert_render_snapshot!(harness, "prose_alignment_flex");
}
}