1use std::{borrow::Cow, sync::Arc};
2
3use emath::GuiRounding as _;
4
5use crate::{
6 text::{LayoutJob, TextWrapping},
7 Align, Color32, FontFamily, FontSelection, Galley, Style, TextStyle, TextWrapMode, Ui, Visuals,
8};
9
10#[derive(Debug, Clone, Default, PartialEq)]
26pub struct RichText {
27 text: String,
28 size: Option<f32>,
29 extra_letter_spacing: f32,
30 line_height: Option<f32>,
31 family: Option<FontFamily>,
32 text_style: Option<TextStyle>,
33 background_color: Color32,
34 text_color: Option<Color32>,
35 code: bool,
36 strong: bool,
37 weak: bool,
38 strikethrough: bool,
39 underline: bool,
40 italics: bool,
41 raised: bool,
42}
43
44impl From<&str> for RichText {
45 #[inline]
46 fn from(text: &str) -> Self {
47 Self::new(text)
48 }
49}
50
51impl From<&String> for RichText {
52 #[inline]
53 fn from(text: &String) -> Self {
54 Self::new(text)
55 }
56}
57
58impl From<&mut String> for RichText {
59 #[inline]
60 fn from(text: &mut String) -> Self {
61 Self::new(text.clone())
62 }
63}
64
65impl From<String> for RichText {
66 #[inline]
67 fn from(text: String) -> Self {
68 Self::new(text)
69 }
70}
71
72impl From<&Box<str>> for RichText {
73 #[inline]
74 fn from(text: &Box<str>) -> Self {
75 Self::new(text.clone())
76 }
77}
78
79impl From<&mut Box<str>> for RichText {
80 #[inline]
81 fn from(text: &mut Box<str>) -> Self {
82 Self::new(text.clone())
83 }
84}
85
86impl From<Box<str>> for RichText {
87 #[inline]
88 fn from(text: Box<str>) -> Self {
89 Self::new(text)
90 }
91}
92
93impl From<Cow<'_, str>> for RichText {
94 #[inline]
95 fn from(text: Cow<'_, str>) -> Self {
96 Self::new(text)
97 }
98}
99
100impl RichText {
101 #[inline]
102 pub fn new(text: impl Into<String>) -> Self {
103 Self {
104 text: text.into(),
105 ..Default::default()
106 }
107 }
108
109 #[inline]
110 pub fn is_empty(&self) -> bool {
111 self.text.is_empty()
112 }
113
114 #[inline]
115 pub fn text(&self) -> &str {
116 &self.text
117 }
118
119 #[inline]
122 pub fn size(mut self, size: f32) -> Self {
123 self.size = Some(size);
124 self
125 }
126
127 #[inline]
134 pub fn extra_letter_spacing(mut self, extra_letter_spacing: f32) -> Self {
135 self.extra_letter_spacing = extra_letter_spacing;
136 self
137 }
138
139 #[inline]
148 pub fn line_height(mut self, line_height: Option<f32>) -> Self {
149 self.line_height = line_height;
150 self
151 }
152
153 #[inline]
159 pub fn family(mut self, family: FontFamily) -> Self {
160 self.family = Some(family);
161 self
162 }
163
164 #[inline]
167 pub fn font(mut self, font_id: crate::FontId) -> Self {
168 let crate::FontId { size, family } = font_id;
169 self.size = Some(size);
170 self.family = Some(family);
171 self
172 }
173
174 #[inline]
176 pub fn text_style(mut self, text_style: TextStyle) -> Self {
177 self.text_style = Some(text_style);
178 self
179 }
180
181 #[inline]
183 pub fn fallback_text_style(mut self, text_style: TextStyle) -> Self {
184 self.text_style.get_or_insert(text_style);
185 self
186 }
187
188 #[inline]
190 pub fn heading(self) -> Self {
191 self.text_style(TextStyle::Heading)
192 }
193
194 #[inline]
196 pub fn monospace(self) -> Self {
197 self.text_style(TextStyle::Monospace)
198 }
199
200 #[inline]
202 pub fn code(mut self) -> Self {
203 self.code = true;
204 self.text_style(TextStyle::Monospace)
205 }
206
207 #[inline]
209 pub fn strong(mut self) -> Self {
210 self.strong = true;
211 self
212 }
213
214 #[inline]
216 pub fn weak(mut self) -> Self {
217 self.weak = true;
218 self
219 }
220
221 #[inline]
225 pub fn underline(mut self) -> Self {
226 self.underline = true;
227 self
228 }
229
230 #[inline]
234 pub fn strikethrough(mut self) -> Self {
235 self.strikethrough = true;
236 self
237 }
238
239 #[inline]
241 pub fn italics(mut self) -> Self {
242 self.italics = true;
243 self
244 }
245
246 #[inline]
248 pub fn small(self) -> Self {
249 self.text_style(TextStyle::Small)
250 }
251
252 #[inline]
254 pub fn small_raised(self) -> Self {
255 self.text_style(TextStyle::Small).raised()
256 }
257
258 #[inline]
260 pub fn raised(mut self) -> Self {
261 self.raised = true;
262 self
263 }
264
265 #[inline]
267 pub fn background_color(mut self, background_color: impl Into<Color32>) -> Self {
268 self.background_color = background_color.into();
269 self
270 }
271
272 #[inline]
277 pub fn color(mut self, color: impl Into<Color32>) -> Self {
278 self.text_color = Some(color.into());
279 self
280 }
281
282 pub fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
286 let mut font_id = self.text_style.as_ref().map_or_else(
287 || FontSelection::Default.resolve(style),
288 |text_style| text_style.resolve(style),
289 );
290
291 if let Some(size) = self.size {
292 font_id.size = size;
293 }
294 if let Some(family) = &self.family {
295 font_id.family = family.clone();
296 }
297 fonts.row_height(&font_id)
298 }
299
300 pub fn append_to(
330 self,
331 layout_job: &mut LayoutJob,
332 style: &Style,
333 fallback_font: FontSelection,
334 default_valign: Align,
335 ) {
336 let (text, format) = self.into_text_and_format(style, fallback_font, default_valign);
337
338 layout_job.append(&text, 0.0, format);
339 }
340
341 fn into_layout_job(
342 self,
343 style: &Style,
344 fallback_font: FontSelection,
345 default_valign: Align,
346 ) -> LayoutJob {
347 let (text, text_format) = self.into_text_and_format(style, fallback_font, default_valign);
348 LayoutJob::single_section(text, text_format)
349 }
350
351 fn into_text_and_format(
352 self,
353 style: &Style,
354 fallback_font: FontSelection,
355 default_valign: Align,
356 ) -> (String, crate::text::TextFormat) {
357 let text_color = self.get_text_color(&style.visuals);
358
359 let Self {
360 text,
361 size,
362 extra_letter_spacing,
363 line_height,
364 family,
365 text_style,
366 background_color,
367 text_color: _, code,
369 strong: _, weak: _, strikethrough,
372 underline,
373 italics,
374 raised,
375 } = self;
376
377 let line_color = text_color.unwrap_or_else(|| style.visuals.text_color());
378 let text_color = text_color.unwrap_or(crate::Color32::PLACEHOLDER);
379
380 let font_id = {
381 let mut font_id = text_style
382 .or_else(|| style.override_text_style.clone())
383 .map_or_else(
384 || fallback_font.resolve(style),
385 |text_style| text_style.resolve(style),
386 );
387 if let Some(fid) = style.override_font_id.clone() {
388 font_id = fid;
389 }
390 if let Some(size) = size {
391 font_id.size = size;
392 }
393 if let Some(family) = family {
394 font_id.family = family;
395 }
396 font_id
397 };
398
399 let mut background_color = background_color;
400 if code {
401 background_color = style.visuals.code_bg_color;
402 }
403 let underline = if underline {
404 crate::Stroke::new(1.0, line_color)
405 } else {
406 crate::Stroke::NONE
407 };
408 let strikethrough = if strikethrough {
409 crate::Stroke::new(1.0, line_color)
410 } else {
411 crate::Stroke::NONE
412 };
413
414 let valign = if raised {
415 crate::Align::TOP
416 } else {
417 default_valign
418 };
419
420 (
421 text,
422 crate::text::TextFormat {
423 font_id,
424 extra_letter_spacing,
425 line_height,
426 color: text_color,
427 background: background_color,
428 italics,
429 underline,
430 strikethrough,
431 valign,
432 },
433 )
434 }
435
436 fn get_text_color(&self, visuals: &Visuals) -> Option<Color32> {
437 if let Some(text_color) = self.text_color {
438 Some(text_color)
439 } else if self.strong {
440 Some(visuals.strong_text_color())
441 } else if self.weak {
442 Some(visuals.weak_text_color())
443 } else {
444 visuals.override_text_color
445 }
446 }
447}
448
449#[derive(Clone)]
464pub enum WidgetText {
465 RichText(RichText),
466
467 LayoutJob(LayoutJob),
480
481 Galley(Arc<Galley>),
486}
487
488impl Default for WidgetText {
489 fn default() -> Self {
490 Self::RichText(RichText::default())
491 }
492}
493
494impl WidgetText {
495 #[inline]
496 pub fn is_empty(&self) -> bool {
497 match self {
498 Self::RichText(text) => text.is_empty(),
499 Self::LayoutJob(job) => job.is_empty(),
500 Self::Galley(galley) => galley.is_empty(),
501 }
502 }
503
504 #[inline]
505 pub fn text(&self) -> &str {
506 match self {
507 Self::RichText(text) => text.text(),
508 Self::LayoutJob(job) => &job.text,
509 Self::Galley(galley) => galley.text(),
510 }
511 }
512
513 #[inline]
517 pub fn text_style(self, text_style: TextStyle) -> Self {
518 match self {
519 Self::RichText(text) => Self::RichText(text.text_style(text_style)),
520 Self::LayoutJob(_) | Self::Galley(_) => self,
521 }
522 }
523
524 #[inline]
528 pub fn fallback_text_style(self, text_style: TextStyle) -> Self {
529 match self {
530 Self::RichText(text) => Self::RichText(text.fallback_text_style(text_style)),
531 Self::LayoutJob(_) | Self::Galley(_) => self,
532 }
533 }
534
535 #[inline]
539 pub fn color(self, color: impl Into<Color32>) -> Self {
540 match self {
541 Self::RichText(text) => Self::RichText(text.color(color)),
542 Self::LayoutJob(_) | Self::Galley(_) => self,
543 }
544 }
545
546 pub fn heading(self) -> Self {
548 match self {
549 Self::RichText(text) => Self::RichText(text.heading()),
550 Self::LayoutJob(_) | Self::Galley(_) => self,
551 }
552 }
553
554 pub fn monospace(self) -> Self {
556 match self {
557 Self::RichText(text) => Self::RichText(text.monospace()),
558 Self::LayoutJob(_) | Self::Galley(_) => self,
559 }
560 }
561
562 pub fn code(self) -> Self {
564 match self {
565 Self::RichText(text) => Self::RichText(text.code()),
566 Self::LayoutJob(_) | Self::Galley(_) => self,
567 }
568 }
569
570 pub fn strong(self) -> Self {
572 match self {
573 Self::RichText(text) => Self::RichText(text.strong()),
574 Self::LayoutJob(_) | Self::Galley(_) => self,
575 }
576 }
577
578 pub fn weak(self) -> Self {
580 match self {
581 Self::RichText(text) => Self::RichText(text.weak()),
582 Self::LayoutJob(_) | Self::Galley(_) => self,
583 }
584 }
585
586 pub fn underline(self) -> Self {
588 match self {
589 Self::RichText(text) => Self::RichText(text.underline()),
590 Self::LayoutJob(_) | Self::Galley(_) => self,
591 }
592 }
593
594 pub fn strikethrough(self) -> Self {
596 match self {
597 Self::RichText(text) => Self::RichText(text.strikethrough()),
598 Self::LayoutJob(_) | Self::Galley(_) => self,
599 }
600 }
601
602 pub fn italics(self) -> Self {
604 match self {
605 Self::RichText(text) => Self::RichText(text.italics()),
606 Self::LayoutJob(_) | Self::Galley(_) => self,
607 }
608 }
609
610 pub fn small(self) -> Self {
612 match self {
613 Self::RichText(text) => Self::RichText(text.small()),
614 Self::LayoutJob(_) | Self::Galley(_) => self,
615 }
616 }
617
618 pub fn small_raised(self) -> Self {
620 match self {
621 Self::RichText(text) => Self::RichText(text.small_raised()),
622 Self::LayoutJob(_) | Self::Galley(_) => self,
623 }
624 }
625
626 pub fn raised(self) -> Self {
628 match self {
629 Self::RichText(text) => Self::RichText(text.raised()),
630 Self::LayoutJob(_) | Self::Galley(_) => self,
631 }
632 }
633
634 pub fn background_color(self, background_color: impl Into<Color32>) -> Self {
636 match self {
637 Self::RichText(text) => Self::RichText(text.background_color(background_color)),
638 Self::LayoutJob(_) | Self::Galley(_) => self,
639 }
640 }
641
642 pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
644 match self {
645 Self::RichText(text) => text.font_height(fonts, style),
646 Self::LayoutJob(job) => job.font_height(fonts),
647 Self::Galley(galley) => {
648 if let Some(row) = galley.rows.first() {
649 row.height().round_ui()
650 } else {
651 galley.size().y.round_ui()
652 }
653 }
654 }
655 }
656
657 pub fn into_layout_job(
658 self,
659 style: &Style,
660 fallback_font: FontSelection,
661 default_valign: Align,
662 ) -> LayoutJob {
663 match self {
664 Self::RichText(text) => text.into_layout_job(style, fallback_font, default_valign),
665 Self::LayoutJob(job) => job,
666 Self::Galley(galley) => (*galley.job).clone(),
667 }
668 }
669
670 pub fn into_galley(
674 self,
675 ui: &Ui,
676 wrap_mode: Option<TextWrapMode>,
677 available_width: f32,
678 fallback_font: impl Into<FontSelection>,
679 ) -> Arc<Galley> {
680 let valign = ui.text_valign();
681 let style = ui.style();
682
683 let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
684 let text_wrapping = TextWrapping::from_wrap_mode_and_width(wrap_mode, available_width);
685
686 self.into_galley_impl(ui.ctx(), style, text_wrapping, fallback_font.into(), valign)
687 }
688
689 pub fn into_galley_impl(
690 self,
691 ctx: &crate::Context,
692 style: &Style,
693 text_wrapping: TextWrapping,
694 fallback_font: FontSelection,
695 default_valign: Align,
696 ) -> Arc<Galley> {
697 match self {
698 Self::RichText(text) => {
699 let mut layout_job = text.into_layout_job(style, fallback_font, default_valign);
700 layout_job.wrap = text_wrapping;
701 ctx.fonts(|f| f.layout_job(layout_job))
702 }
703 Self::LayoutJob(mut job) => {
704 job.wrap = text_wrapping;
705 ctx.fonts(|f| f.layout_job(job))
706 }
707 Self::Galley(galley) => galley,
708 }
709 }
710}
711
712impl From<&str> for WidgetText {
713 #[inline]
714 fn from(text: &str) -> Self {
715 Self::RichText(RichText::new(text))
716 }
717}
718
719impl From<&String> for WidgetText {
720 #[inline]
721 fn from(text: &String) -> Self {
722 Self::RichText(RichText::new(text))
723 }
724}
725
726impl From<String> for WidgetText {
727 #[inline]
728 fn from(text: String) -> Self {
729 Self::RichText(RichText::new(text))
730 }
731}
732
733impl From<&Box<str>> for WidgetText {
734 #[inline]
735 fn from(text: &Box<str>) -> Self {
736 Self::RichText(RichText::new(text.clone()))
737 }
738}
739
740impl From<Box<str>> for WidgetText {
741 #[inline]
742 fn from(text: Box<str>) -> Self {
743 Self::RichText(RichText::new(text))
744 }
745}
746
747impl From<Cow<'_, str>> for WidgetText {
748 #[inline]
749 fn from(text: Cow<'_, str>) -> Self {
750 Self::RichText(RichText::new(text))
751 }
752}
753
754impl From<RichText> for WidgetText {
755 #[inline]
756 fn from(rich_text: RichText) -> Self {
757 Self::RichText(rich_text)
758 }
759}
760
761impl From<LayoutJob> for WidgetText {
762 #[inline]
763 fn from(layout_job: LayoutJob) -> Self {
764 Self::LayoutJob(layout_job)
765 }
766}
767
768impl From<Arc<Galley>> for WidgetText {
769 #[inline]
770 fn from(galley: Arc<Galley>) -> Self {
771 Self::Galley(galley)
772 }
773}