// 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::widget::{Padding, TextArea, WidgetMut, WidgetPod}; use crate::{ AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, PaintCtx, PointerEvent, QueryCtx, RegisterCtx, TextEvent, Update, UpdateCtx, Widget, WidgetId, }; /// 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>(this: &'t mut WidgetMut<'_, Self>) -> WidgetMut<'t, TextArea<false>> { this.ctx.get_mut(&mut this.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(this: &mut WidgetMut<'_, Self>, clip: bool) { this.widget.clip = clip; this.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::widget::{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"); } }