1use crate::core::alignment;
2use crate::core::event;
3use crate::core::layout;
4use crate::core::mouse;
5use crate::core::renderer;
6use crate::core::text::{Paragraph, Span};
7use crate::core::widget::text::{
8 self, Catalog, LineHeight, Shaping, Style, StyleFn, Wrapping,
9};
10use crate::core::widget::tree::{self, Tree};
11use crate::core::{
12 self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point,
13 Rectangle, Shell, Size, Vector, Widget,
14};
15
16#[allow(missing_debug_implementations)]
18pub struct Rich<'a, Link, Theme = crate::Theme, Renderer = crate::Renderer>
19where
20 Link: Clone + 'static,
21 Theme: Catalog,
22 Renderer: core::text::Renderer,
23{
24 spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>,
25 size: Option<Pixels>,
26 line_height: LineHeight,
27 width: Length,
28 height: Length,
29 font: Option<Renderer::Font>,
30 align_x: alignment::Horizontal,
31 align_y: alignment::Vertical,
32 wrapping: Wrapping,
33 class: Theme::Class<'a>,
34}
35
36impl<'a, Link, Theme, Renderer> Rich<'a, Link, Theme, Renderer>
37where
38 Link: Clone + 'static,
39 Theme: Catalog,
40 Renderer: core::text::Renderer,
41 Renderer::Font: 'a,
42{
43 pub fn new() -> Self {
45 Self {
46 spans: Box::new([]),
47 size: None,
48 line_height: LineHeight::default(),
49 width: Length::Shrink,
50 height: Length::Shrink,
51 font: None,
52 align_x: alignment::Horizontal::Left,
53 align_y: alignment::Vertical::Top,
54 wrapping: Wrapping::default(),
55 class: Theme::default(),
56 }
57 }
58
59 pub fn with_spans(
61 spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a,
62 ) -> Self {
63 Self {
64 spans: Box::new(spans),
65 ..Self::new()
66 }
67 }
68
69 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
71 self.size = Some(size.into());
72 self
73 }
74
75 pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
77 self.line_height = line_height.into();
78 self
79 }
80
81 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
83 self.font = Some(font.into());
84 self
85 }
86
87 pub fn width(mut self, width: impl Into<Length>) -> Self {
89 self.width = width.into();
90 self
91 }
92
93 pub fn height(mut self, height: impl Into<Length>) -> Self {
95 self.height = height.into();
96 self
97 }
98
99 pub fn center(self) -> Self {
101 self.align_x(alignment::Horizontal::Center)
102 .align_y(alignment::Vertical::Center)
103 }
104
105 pub fn align_x(
107 mut self,
108 alignment: impl Into<alignment::Horizontal>,
109 ) -> Self {
110 self.align_x = alignment.into();
111 self
112 }
113
114 pub fn align_y(
116 mut self,
117 alignment: impl Into<alignment::Vertical>,
118 ) -> Self {
119 self.align_y = alignment.into();
120 self
121 }
122
123 pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
125 self.wrapping = wrapping;
126 self
127 }
128
129 #[must_use]
131 pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
132 where
133 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
134 {
135 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
136 self
137 }
138
139 pub fn color(self, color: impl Into<Color>) -> Self
141 where
142 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
143 {
144 self.color_maybe(Some(color))
145 }
146
147 pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self
149 where
150 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
151 {
152 let color = color.map(Into::into);
153
154 self.style(move |_theme| Style { color })
155 }
156
157 #[cfg(feature = "advanced")]
159 #[must_use]
160 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
161 self.class = class.into();
162 self
163 }
164}
165
166impl<'a, Link, Theme, Renderer> Default for Rich<'a, Link, Theme, Renderer>
167where
168 Link: Clone + 'a,
169 Theme: Catalog,
170 Renderer: core::text::Renderer,
171 Renderer::Font: 'a,
172{
173 fn default() -> Self {
174 Self::new()
175 }
176}
177
178struct State<Link, P: Paragraph> {
179 spans: Vec<Span<'static, Link, P::Font>>,
180 span_pressed: Option<usize>,
181 paragraph: P,
182}
183
184impl<'a, Link, Theme, Renderer> Widget<Link, Theme, Renderer>
185 for Rich<'a, Link, Theme, Renderer>
186where
187 Link: Clone + 'static,
188 Theme: Catalog,
189 Renderer: core::text::Renderer,
190{
191 fn tag(&self) -> tree::Tag {
192 tree::Tag::of::<State<Link, Renderer::Paragraph>>()
193 }
194
195 fn state(&self) -> tree::State {
196 tree::State::new(State::<Link, _> {
197 spans: Vec::new(),
198 span_pressed: None,
199 paragraph: Renderer::Paragraph::default(),
200 })
201 }
202
203 fn size(&self) -> Size<Length> {
204 Size {
205 width: self.width,
206 height: self.height,
207 }
208 }
209
210 fn layout(
211 &self,
212 tree: &mut Tree,
213 renderer: &Renderer,
214 limits: &layout::Limits,
215 ) -> layout::Node {
216 layout(
217 tree.state
218 .downcast_mut::<State<Link, Renderer::Paragraph>>(),
219 renderer,
220 limits,
221 self.width,
222 self.height,
223 self.spans.as_ref().as_ref(),
224 self.line_height,
225 self.size,
226 self.font,
227 self.align_x,
228 self.align_y,
229 self.wrapping,
230 )
231 }
232
233 fn draw(
234 &self,
235 tree: &Tree,
236 renderer: &mut Renderer,
237 theme: &Theme,
238 defaults: &renderer::Style,
239 layout: Layout<'_>,
240 cursor: mouse::Cursor,
241 viewport: &Rectangle,
242 ) {
243 let state = tree
244 .state
245 .downcast_ref::<State<Link, Renderer::Paragraph>>();
246
247 let style = theme.style(&self.class);
248
249 let hovered_span = cursor
250 .position_in(layout.bounds())
251 .and_then(|position| state.paragraph.hit_span(position));
252
253 for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
254 let is_hovered_link =
255 span.link.is_some() && Some(index) == hovered_span;
256
257 if span.highlight.is_some()
258 || span.underline
259 || span.strikethrough
260 || is_hovered_link
261 {
262 let translation = layout.position() - Point::ORIGIN;
263 let regions = state.paragraph.span_bounds(index);
264
265 if let Some(highlight) = span.highlight {
266 for bounds in ®ions {
267 let bounds = Rectangle::new(
268 bounds.position()
269 - Vector::new(
270 span.padding.left,
271 span.padding.top,
272 ),
273 bounds.size()
274 + Size::new(
275 span.padding.horizontal(),
276 span.padding.vertical(),
277 ),
278 );
279
280 renderer.fill_quad(
281 renderer::Quad {
282 bounds: bounds + translation,
283 border: highlight.border,
284 ..Default::default()
285 },
286 highlight.background,
287 );
288 }
289 }
290
291 if span.underline || span.strikethrough || is_hovered_link {
292 let size = span
293 .size
294 .or(self.size)
295 .unwrap_or(renderer.default_size());
296
297 let line_height = span
298 .line_height
299 .unwrap_or(self.line_height)
300 .to_absolute(size);
301
302 let color = span
303 .color
304 .or(style.color)
305 .unwrap_or(defaults.text_color);
306
307 let baseline = translation
308 + Vector::new(
309 0.0,
310 size.0 + (line_height.0 - size.0) / 2.0,
311 );
312
313 if span.underline || is_hovered_link {
314 for bounds in ®ions {
315 renderer.fill_quad(
316 renderer::Quad {
317 bounds: Rectangle::new(
318 bounds.position() + baseline
319 - Vector::new(0.0, size.0 * 0.08),
320 Size::new(bounds.width, 1.0),
321 ),
322 ..Default::default()
323 },
324 color,
325 );
326 }
327 }
328
329 if span.strikethrough {
330 for bounds in ®ions {
331 renderer.fill_quad(
332 renderer::Quad {
333 bounds: Rectangle::new(
334 bounds.position() + baseline
335 - Vector::new(0.0, size.0 / 2.0),
336 Size::new(bounds.width, 1.0),
337 ),
338 ..Default::default()
339 },
340 color,
341 );
342 }
343 }
344 }
345 }
346 }
347
348 text::draw(
349 renderer,
350 defaults,
351 layout,
352 &state.paragraph,
353 style,
354 viewport,
355 );
356 }
357
358 fn on_event(
359 &mut self,
360 tree: &mut Tree,
361 event: Event,
362 layout: Layout<'_>,
363 cursor: mouse::Cursor,
364 _renderer: &Renderer,
365 _clipboard: &mut dyn Clipboard,
366 shell: &mut Shell<'_, Link>,
367 _viewport: &Rectangle,
368 ) -> event::Status {
369 match event {
370 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
371 if let Some(position) = cursor.position_in(layout.bounds()) {
372 let state = tree
373 .state
374 .downcast_mut::<State<Link, Renderer::Paragraph>>();
375
376 if let Some(span) = state.paragraph.hit_span(position) {
377 if self
378 .spans
379 .as_ref()
380 .as_ref()
381 .get(span)
382 .map_or(false, |span| span.link.is_some())
383 {
384 state.span_pressed = Some(span);
385
386 return event::Status::Captured;
387 }
388 }
389 }
390 }
391 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
392 let state = tree
393 .state
394 .downcast_mut::<State<Link, Renderer::Paragraph>>();
395
396 if let Some(span_pressed) = state.span_pressed {
397 state.span_pressed = None;
398
399 if let Some(position) = cursor.position_in(layout.bounds())
400 {
401 match state.paragraph.hit_span(position) {
402 Some(span) if span == span_pressed => {
403 if let Some(link) = self
404 .spans
405 .as_ref()
406 .as_ref()
407 .get(span)
408 .and_then(|span| span.link.clone())
409 {
410 shell.publish(link);
411 }
412 }
413 _ => {}
414 }
415 }
416 }
417 }
418 _ => {}
419 }
420
421 event::Status::Ignored
422 }
423
424 fn mouse_interaction(
425 &self,
426 tree: &Tree,
427 layout: Layout<'_>,
428 cursor: mouse::Cursor,
429 _viewport: &Rectangle,
430 _renderer: &Renderer,
431 ) -> mouse::Interaction {
432 if let Some(position) = cursor.position_in(layout.bounds()) {
433 let state = tree
434 .state
435 .downcast_ref::<State<Link, Renderer::Paragraph>>();
436
437 if let Some(span) = state
438 .paragraph
439 .hit_span(position)
440 .and_then(|span| self.spans.as_ref().as_ref().get(span))
441 {
442 if span.link.is_some() {
443 return mouse::Interaction::Pointer;
444 }
445 }
446 }
447
448 mouse::Interaction::None
449 }
450}
451
452fn layout<Link, Renderer>(
453 state: &mut State<Link, Renderer::Paragraph>,
454 renderer: &Renderer,
455 limits: &layout::Limits,
456 width: Length,
457 height: Length,
458 spans: &[Span<'_, Link, Renderer::Font>],
459 line_height: LineHeight,
460 size: Option<Pixels>,
461 font: Option<Renderer::Font>,
462 horizontal_alignment: alignment::Horizontal,
463 vertical_alignment: alignment::Vertical,
464 wrapping: Wrapping,
465) -> layout::Node
466where
467 Link: Clone,
468 Renderer: core::text::Renderer,
469{
470 layout::sized(limits, width, height, |limits| {
471 let bounds = limits.max();
472
473 let size = size.unwrap_or_else(|| renderer.default_size());
474 let font = font.unwrap_or_else(|| renderer.default_font());
475
476 let text_with_spans = || core::Text {
477 content: spans,
478 bounds,
479 size,
480 line_height,
481 font,
482 horizontal_alignment,
483 vertical_alignment,
484 shaping: Shaping::Advanced,
485 wrapping,
486 };
487
488 if state.spans != spans {
489 state.paragraph =
490 Renderer::Paragraph::with_spans(text_with_spans());
491 state.spans = spans.iter().cloned().map(Span::to_static).collect();
492 } else {
493 match state.paragraph.compare(core::Text {
494 content: (),
495 bounds,
496 size,
497 line_height,
498 font,
499 horizontal_alignment,
500 vertical_alignment,
501 shaping: Shaping::Advanced,
502 wrapping,
503 }) {
504 core::text::Difference::None => {}
505 core::text::Difference::Bounds => {
506 state.paragraph.resize(bounds);
507 }
508 core::text::Difference::Shape => {
509 state.paragraph =
510 Renderer::Paragraph::with_spans(text_with_spans());
511 }
512 }
513 }
514
515 state.paragraph.min_bounds()
516 })
517}
518
519impl<'a, Link, Theme, Renderer> FromIterator<Span<'a, Link, Renderer::Font>>
520 for Rich<'a, Link, Theme, Renderer>
521where
522 Link: Clone + 'a,
523 Theme: Catalog,
524 Renderer: core::text::Renderer,
525 Renderer::Font: 'a,
526{
527 fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>(
528 spans: T,
529 ) -> Self {
530 Self::with_spans(spans.into_iter().collect::<Vec<_>>())
531 }
532}
533
534impl<'a, Link, Theme, Renderer> From<Rich<'a, Link, Theme, Renderer>>
535 for Element<'a, Link, Theme, Renderer>
536where
537 Link: Clone + 'a,
538 Theme: Catalog + 'a,
539 Renderer: core::text::Renderer + 'a,
540{
541 fn from(
542 text: Rich<'a, Link, Theme, Renderer>,
543 ) -> Element<'a, Link, Theme, Renderer> {
544 Element::new(text)
545 }
546}