-
Notifications
You must be signed in to change notification settings - Fork 127
/
Copy pathprose.rs
223 lines (189 loc) · 7.75 KB
/
prose.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// 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");
}
}