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