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