1use std::{borrow::Cow, sync::Arc};
2
3use crate::{
4 text::{LayoutJob, TextWrapping},
5 Align, Color32, FontFamily, FontSelection, Galley, Style, TextStyle, TextWrapMode, Ui, Visuals,
6};
7
8#[derive(Clone, Default, PartialEq)]
24pub struct RichText {
25 text: String,
26 size: Option<f32>,
27 extra_letter_spacing: f32,
28 line_height: Option<f32>,
29 family: Option<FontFamily>,
30 text_style: Option<TextStyle>,
31 background_color: Color32,
32 text_color: Option<Color32>,
33 code: bool,
34 strong: bool,
35 weak: bool,
36 strikethrough: bool,
37 underline: bool,
38 italics: bool,
39 raised: bool,
40}
41
42impl From<&str> for RichText {
43 #[inline]
44 fn from(text: &str) -> Self {
45 Self::new(text)
46 }
47}
48
49impl From<&String> for RichText {
50 #[inline]
51 fn from(text: &String) -> Self {
52 Self::new(text)
53 }
54}
55
56impl From<&mut String> for RichText {
57 #[inline]
58 fn from(text: &mut String) -> Self {
59 Self::new(text.clone())
60 }
61}
62
63impl From<String> for RichText {
64 #[inline]
65 fn from(text: String) -> Self {
66 Self::new(text)
67 }
68}
69
70impl From<Cow<'_, str>> for RichText {
71 #[inline]
72 fn from(text: Cow<'_, str>) -> Self {
73 Self::new(text)
74 }
75}
76
77impl RichText {
78 #[inline]
79 pub fn new(text: impl Into<String>) -> Self {
80 Self {
81 text: text.into(),
82 ..Default::default()
83 }
84 }
85
86 #[inline]
87 pub fn is_empty(&self) -> bool {
88 self.text.is_empty()
89 }
90
91 #[inline]
92 pub fn text(&self) -> &str {
93 &self.text
94 }
95
96 #[inline]
99 pub fn size(mut self, size: f32) -> Self {
100 self.size = Some(size);
101 self
102 }
103
104 #[inline]
111 pub fn extra_letter_spacing(mut self, extra_letter_spacing: f32) -> Self {
112 self.extra_letter_spacing = extra_letter_spacing;
113 self
114 }
115
116 #[inline]
125 pub fn line_height(mut self, line_height: Option<f32>) -> Self {
126 self.line_height = line_height;
127 self
128 }
129
130 #[inline]
136 pub fn family(mut self, family: FontFamily) -> Self {
137 self.family = Some(family);
138 self
139 }
140
141 #[inline]
144 pub fn font(mut self, font_id: crate::FontId) -> Self {
145 let crate::FontId { size, family } = font_id;
146 self.size = Some(size);
147 self.family = Some(family);
148 self
149 }
150
151 #[inline]
153 pub fn text_style(mut self, text_style: TextStyle) -> Self {
154 self.text_style = Some(text_style);
155 self
156 }
157
158 #[inline]
160 pub fn fallback_text_style(mut self, text_style: TextStyle) -> Self {
161 self.text_style.get_or_insert(text_style);
162 self
163 }
164
165 #[inline]
167 pub fn heading(self) -> Self {
168 self.text_style(TextStyle::Heading)
169 }
170
171 #[inline]
173 pub fn monospace(self) -> Self {
174 self.text_style(TextStyle::Monospace)
175 }
176
177 #[inline]
179 pub fn code(mut self) -> Self {
180 self.code = true;
181 self.text_style(TextStyle::Monospace)
182 }
183
184 #[inline]
186 pub fn strong(mut self) -> Self {
187 self.strong = true;
188 self
189 }
190
191 #[inline]
193 pub fn weak(mut self) -> Self {
194 self.weak = true;
195 self
196 }
197
198 #[inline]
202 pub fn underline(mut self) -> Self {
203 self.underline = true;
204 self
205 }
206
207 #[inline]
211 pub fn strikethrough(mut self) -> Self {
212 self.strikethrough = true;
213 self
214 }
215
216 #[inline]
218 pub fn italics(mut self) -> Self {
219 self.italics = true;
220 self
221 }
222
223 #[inline]
225 pub fn small(self) -> Self {
226 self.text_style(TextStyle::Small)
227 }
228
229 #[inline]
231 pub fn small_raised(self) -> Self {
232 self.text_style(TextStyle::Small).raised()
233 }
234
235 #[inline]
237 pub fn raised(mut self) -> Self {
238 self.raised = true;
239 self
240 }
241
242 #[inline]
244 pub fn background_color(mut self, background_color: impl Into<Color32>) -> Self {
245 self.background_color = background_color.into();
246 self
247 }
248
249 #[inline]
254 pub fn color(mut self, color: impl Into<Color32>) -> Self {
255 self.text_color = Some(color.into());
256 self
257 }
258
259 pub fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
261 let mut font_id = self.text_style.as_ref().map_or_else(
262 || FontSelection::Default.resolve(style),
263 |text_style| text_style.resolve(style),
264 );
265
266 if let Some(size) = self.size {
267 font_id.size = size;
268 }
269 if let Some(family) = &self.family {
270 font_id.family = family.clone();
271 }
272 fonts.row_height(&font_id)
273 }
274
275 pub fn append_to(
305 self,
306 layout_job: &mut LayoutJob,
307 style: &Style,
308 fallback_font: FontSelection,
309 default_valign: Align,
310 ) {
311 let (text, format) = self.into_text_and_format(style, fallback_font, default_valign);
312
313 layout_job.append(&text, 0.0, format);
314 }
315
316 fn into_layout_job(
317 self,
318 style: &Style,
319 fallback_font: FontSelection,
320 default_valign: Align,
321 ) -> LayoutJob {
322 let (text, text_format) = self.into_text_and_format(style, fallback_font, default_valign);
323 LayoutJob::single_section(text, text_format)
324 }
325
326 fn into_text_and_format(
327 self,
328 style: &Style,
329 fallback_font: FontSelection,
330 default_valign: Align,
331 ) -> (String, crate::text::TextFormat) {
332 let text_color = self.get_text_color(&style.visuals);
333
334 let Self {
335 text,
336 size,
337 extra_letter_spacing,
338 line_height,
339 family,
340 text_style,
341 background_color,
342 text_color: _, code,
344 strong: _, weak: _, strikethrough,
347 underline,
348 italics,
349 raised,
350 } = self;
351
352 let line_color = text_color.unwrap_or_else(|| style.visuals.text_color());
353 let text_color = text_color.unwrap_or(crate::Color32::PLACEHOLDER);
354
355 let font_id = {
356 let mut font_id = text_style
357 .or_else(|| style.override_text_style.clone())
358 .map_or_else(
359 || fallback_font.resolve(style),
360 |text_style| text_style.resolve(style),
361 );
362 if let Some(size) = size {
363 font_id.size = size;
364 }
365 if let Some(family) = family {
366 font_id.family = family;
367 }
368 font_id
369 };
370
371 let mut background_color = background_color;
372 if code {
373 background_color = style.visuals.code_bg_color;
374 }
375 let underline = if underline {
376 crate::Stroke::new(1.0, line_color)
377 } else {
378 crate::Stroke::NONE
379 };
380 let strikethrough = if strikethrough {
381 crate::Stroke::new(1.0, line_color)
382 } else {
383 crate::Stroke::NONE
384 };
385
386 let valign = if raised {
387 crate::Align::TOP
388 } else {
389 default_valign
390 };
391
392 (
393 text,
394 crate::text::TextFormat {
395 font_id,
396 extra_letter_spacing,
397 line_height,
398 color: text_color,
399 background: background_color,
400 italics,
401 underline,
402 strikethrough,
403 valign,
404 },
405 )
406 }
407
408 fn get_text_color(&self, visuals: &Visuals) -> Option<Color32> {
409 if let Some(text_color) = self.text_color {
410 Some(text_color)
411 } else if self.strong {
412 Some(visuals.strong_text_color())
413 } else if self.weak {
414 Some(visuals.weak_text_color())
415 } else {
416 visuals.override_text_color
417 }
418 }
419}
420
421#[derive(Clone)]
436pub enum WidgetText {
437 RichText(RichText),
438
439 LayoutJob(LayoutJob),
452
453 Galley(Arc<Galley>),
458}
459
460impl Default for WidgetText {
461 fn default() -> Self {
462 Self::RichText(RichText::default())
463 }
464}
465
466impl WidgetText {
467 #[inline]
468 pub fn is_empty(&self) -> bool {
469 match self {
470 Self::RichText(text) => text.is_empty(),
471 Self::LayoutJob(job) => job.is_empty(),
472 Self::Galley(galley) => galley.is_empty(),
473 }
474 }
475
476 #[inline]
477 pub fn text(&self) -> &str {
478 match self {
479 Self::RichText(text) => text.text(),
480 Self::LayoutJob(job) => &job.text,
481 Self::Galley(galley) => galley.text(),
482 }
483 }
484
485 #[inline]
489 pub fn text_style(self, text_style: TextStyle) -> Self {
490 match self {
491 Self::RichText(text) => Self::RichText(text.text_style(text_style)),
492 Self::LayoutJob(_) | Self::Galley(_) => self,
493 }
494 }
495
496 #[inline]
500 pub fn fallback_text_style(self, text_style: TextStyle) -> Self {
501 match self {
502 Self::RichText(text) => Self::RichText(text.fallback_text_style(text_style)),
503 Self::LayoutJob(_) | Self::Galley(_) => self,
504 }
505 }
506
507 #[inline]
511 pub fn color(self, color: impl Into<Color32>) -> Self {
512 match self {
513 Self::RichText(text) => Self::RichText(text.color(color)),
514 Self::LayoutJob(_) | Self::Galley(_) => self,
515 }
516 }
517
518 pub fn heading(self) -> Self {
520 match self {
521 Self::RichText(text) => Self::RichText(text.heading()),
522 Self::LayoutJob(_) | Self::Galley(_) => self,
523 }
524 }
525
526 pub fn monospace(self) -> Self {
528 match self {
529 Self::RichText(text) => Self::RichText(text.monospace()),
530 Self::LayoutJob(_) | Self::Galley(_) => self,
531 }
532 }
533
534 pub fn code(self) -> Self {
536 match self {
537 Self::RichText(text) => Self::RichText(text.code()),
538 Self::LayoutJob(_) | Self::Galley(_) => self,
539 }
540 }
541
542 pub fn strong(self) -> Self {
544 match self {
545 Self::RichText(text) => Self::RichText(text.strong()),
546 Self::LayoutJob(_) | Self::Galley(_) => self,
547 }
548 }
549
550 pub fn weak(self) -> Self {
552 match self {
553 Self::RichText(text) => Self::RichText(text.weak()),
554 Self::LayoutJob(_) | Self::Galley(_) => self,
555 }
556 }
557
558 pub fn underline(self) -> Self {
560 match self {
561 Self::RichText(text) => Self::RichText(text.underline()),
562 Self::LayoutJob(_) | Self::Galley(_) => self,
563 }
564 }
565
566 pub fn strikethrough(self) -> Self {
568 match self {
569 Self::RichText(text) => Self::RichText(text.strikethrough()),
570 Self::LayoutJob(_) | Self::Galley(_) => self,
571 }
572 }
573
574 pub fn italics(self) -> Self {
576 match self {
577 Self::RichText(text) => Self::RichText(text.italics()),
578 Self::LayoutJob(_) | Self::Galley(_) => self,
579 }
580 }
581
582 pub fn small(self) -> Self {
584 match self {
585 Self::RichText(text) => Self::RichText(text.small()),
586 Self::LayoutJob(_) | Self::Galley(_) => self,
587 }
588 }
589
590 pub fn small_raised(self) -> Self {
592 match self {
593 Self::RichText(text) => Self::RichText(text.small_raised()),
594 Self::LayoutJob(_) | Self::Galley(_) => self,
595 }
596 }
597
598 pub fn raised(self) -> Self {
600 match self {
601 Self::RichText(text) => Self::RichText(text.raised()),
602 Self::LayoutJob(_) | Self::Galley(_) => self,
603 }
604 }
605
606 pub fn background_color(self, background_color: impl Into<Color32>) -> Self {
608 match self {
609 Self::RichText(text) => Self::RichText(text.background_color(background_color)),
610 Self::LayoutJob(_) | Self::Galley(_) => self,
611 }
612 }
613
614 pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
615 match self {
616 Self::RichText(text) => text.font_height(fonts, style),
617 Self::LayoutJob(job) => job.font_height(fonts),
618 Self::Galley(galley) => {
619 if let Some(row) = galley.rows.first() {
620 row.height()
621 } else {
622 galley.size().y
623 }
624 }
625 }
626 }
627
628 pub fn into_layout_job(
629 self,
630 style: &Style,
631 fallback_font: FontSelection,
632 default_valign: Align,
633 ) -> LayoutJob {
634 match self {
635 Self::RichText(text) => text.into_layout_job(style, fallback_font, default_valign),
636 Self::LayoutJob(job) => job,
637 Self::Galley(galley) => (*galley.job).clone(),
638 }
639 }
640
641 pub fn into_galley(
645 self,
646 ui: &Ui,
647 wrap_mode: Option<TextWrapMode>,
648 available_width: f32,
649 fallback_font: impl Into<FontSelection>,
650 ) -> Arc<Galley> {
651 let valign = ui.layout().vertical_align();
652 let style = ui.style();
653
654 let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
655 let text_wrapping = TextWrapping::from_wrap_mode_and_width(wrap_mode, available_width);
656
657 self.into_galley_impl(ui.ctx(), style, text_wrapping, fallback_font.into(), valign)
658 }
659
660 pub fn into_galley_impl(
661 self,
662 ctx: &crate::Context,
663 style: &Style,
664 text_wrapping: TextWrapping,
665 fallback_font: FontSelection,
666 default_valign: Align,
667 ) -> Arc<Galley> {
668 match self {
669 Self::RichText(text) => {
670 let mut layout_job = text.into_layout_job(style, fallback_font, default_valign);
671 layout_job.wrap = text_wrapping;
672 ctx.fonts(|f| f.layout_job(layout_job))
673 }
674 Self::LayoutJob(mut job) => {
675 job.wrap = text_wrapping;
676 ctx.fonts(|f| f.layout_job(job))
677 }
678 Self::Galley(galley) => galley,
679 }
680 }
681}
682
683impl From<&str> for WidgetText {
684 #[inline]
685 fn from(text: &str) -> Self {
686 Self::RichText(RichText::new(text))
687 }
688}
689
690impl From<&String> for WidgetText {
691 #[inline]
692 fn from(text: &String) -> Self {
693 Self::RichText(RichText::new(text))
694 }
695}
696
697impl From<String> for WidgetText {
698 #[inline]
699 fn from(text: String) -> Self {
700 Self::RichText(RichText::new(text))
701 }
702}
703
704impl From<Cow<'_, str>> for WidgetText {
705 #[inline]
706 fn from(text: Cow<'_, str>) -> Self {
707 Self::RichText(RichText::new(text))
708 }
709}
710
711impl From<RichText> for WidgetText {
712 #[inline]
713 fn from(rich_text: RichText) -> Self {
714 Self::RichText(rich_text)
715 }
716}
717
718impl From<LayoutJob> for WidgetText {
719 #[inline]
720 fn from(layout_job: LayoutJob) -> Self {
721 Self::LayoutJob(layout_job)
722 }
723}
724
725impl From<Arc<Galley>> for WidgetText {
726 #[inline]
727 fn from(galley: Arc<Galley>) -> Self {
728 Self::Galley(galley)
729 }
730}