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