mirror of https://github.com/linebender/xilem
224 lines
7.8 KiB
Rust
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");
|
|
}
|
|
}
|