1use std::rc::Rc;
2
3use sequoia_core::Edges;
4use sequoia_core::{Color, Length, Size};
5use sequoia_input::{CursorIcon, Event, EventPhase, PointerButton, PointerEvent};
6use sequoia_layout::{LayoutCtx, LayoutStyle};
7use sequoia_text::{LetterSpacing, LineHeight, RichAlign, TextSpan, TextVAlign, WrapMode};
8
9use crate::view::{EventCtx, PaintCtx, View};
10use crate::views::{Unit, text_wrap_limit};
11
12pub struct RichText {
13 spans: Box<dyn Fn() -> Vec<TextSpan>>,
14 font_size: Option<f32>,
15 color: Option<Color>,
16 wrap_width: Option<f32>,
17 wrap_mode: WrapMode,
18 align: RichAlign,
19 align_y: TextVAlign,
20 line_height: LineHeight,
21 max_lines: Option<usize>,
22 font_scale: f32,
23 auto_wrap: bool,
24 letter_spacing: f32,
25 on_span_click: Option<Rc<dyn Fn(usize)>>,
26 style: LayoutStyle,
27}
28
29impl RichText {
30 pub fn new(spans: Vec<TextSpan>) -> Self {
31 Self {
32 spans: Box::new(move || spans.clone()),
33 font_size: None,
34 color: None,
35 wrap_width: None,
36 wrap_mode: WrapMode::default(),
37 align: RichAlign::Left,
38 align_y: TextVAlign::Top,
39 line_height: LineHeight::default(),
40 max_lines: None,
41 font_scale: 1.0,
42 auto_wrap: false,
43 letter_spacing: 0.0,
44 on_span_click: None,
45 style: LayoutStyle {
46 width: Length::Fit,
47 height: Length::Fit,
48 ..LayoutStyle::default()
49 },
50 }
51 }
52
53 pub fn with(spans: impl Fn() -> Vec<TextSpan> + 'static) -> Self {
54 Self {
55 spans: Box::new(spans),
56 font_size: None,
57 color: None,
58 wrap_width: None,
59 wrap_mode: WrapMode::default(),
60 align: RichAlign::Left,
61 align_y: TextVAlign::Top,
62 line_height: LineHeight::default(),
63 max_lines: None,
64 font_scale: 1.0,
65 auto_wrap: false,
66 letter_spacing: 0.0,
67 on_span_click: None,
68 style: LayoutStyle {
69 width: Length::Fit,
70 height: Length::Fit,
71 ..LayoutStyle::default()
72 },
73 }
74 }
75
76 pub fn spans(&self) -> Vec<TextSpan> {
77 (self.spans)()
78 }
79
80 pub fn span(mut self, span: TextSpan) -> Self {
81 let prev = std::mem::replace(&mut self.spans, Box::new(Vec::new));
82 self.spans = Box::new(move || {
83 let mut out = prev();
84 out.push(span.clone());
85 out
86 });
87 self
88 }
89
90 pub fn size(mut self, size: f32) -> Self {
91 self.font_size = Some(size);
92 self
93 }
94
95 pub fn color(mut self, color: Color) -> Self {
96 self.color = Some(color);
97 self
98 }
99
100 pub fn wrap_width(mut self, width: f32) -> Self {
101 self.wrap_width = if width > 0.0 { Some(width) } else { None };
102 self
103 }
104
105 pub fn wrap_mode(mut self, mode: WrapMode) -> Self {
106 self.wrap_mode = mode;
107 self
108 }
109
110 pub fn wrap(mut self) -> Self {
112 self.auto_wrap = true;
113 self
114 }
115
116 pub fn line_height(mut self, factor: f32) -> Self {
118 if factor.is_finite() && factor > 0.0 {
119 self.line_height = LineHeight::Factor(factor);
120 }
121 self
122 }
123
124 pub fn line_height_px(mut self, px: f32) -> Self {
126 if px.is_finite() && px > 0.0 {
127 self.line_height = LineHeight::Absolute(px);
128 }
129 self
130 }
131
132 pub fn align_y(mut self, align: TextVAlign) -> Self {
134 self.align_y = align;
135 self
136 }
137
138 pub fn max_lines(mut self, lines: usize) -> Self {
140 self.max_lines = if lines == 0 { None } else { Some(lines) };
141 self
142 }
143
144 pub fn font_scale(mut self, scale: f32) -> Self {
146 if scale.is_finite() && scale > 0.0 {
147 self.font_scale = scale;
148 }
149 self
150 }
151
152 pub fn align(mut self, align: RichAlign) -> Self {
153 self.align = align;
154 self
155 }
156
157 pub fn centered(mut self) -> Self {
158 self.align = RichAlign::Center;
159 self
160 }
161
162 pub fn right_aligned(mut self) -> Self {
163 self.align = RichAlign::Right;
164 self
165 }
166
167 pub fn letter_spacing(mut self, spacing: LetterSpacing) -> Self {
168 self.letter_spacing = if spacing.is_finite() { spacing } else { 0.0 };
169 self
170 }
171
172 pub fn on_span_click(mut self, handler: impl Fn(usize) + 'static) -> Self {
173 self.on_span_click = Some(Rc::new(handler));
174 self
175 }
176
177 pub fn width(mut self, width: impl Into<Length>) -> Self {
178 self.style = self.style.width(width.into());
179 self
180 }
181
182 pub fn height(mut self, height: impl Into<Length>) -> Self {
183 self.style = self.style.height(height.into());
184 self
185 }
186
187 pub fn min_width(mut self, v: f32) -> Self {
188 self.style = self.style.min_width(v);
189 self
190 }
191
192 pub fn max_width(mut self, v: f32) -> Self {
193 self.style = self.style.max_width(v);
194 self
195 }
196
197 pub fn min_height(mut self, v: f32) -> Self {
198 self.style = self.style.min_height(v);
199 self
200 }
201
202 pub fn max_height(mut self, v: f32) -> Self {
203 self.style = self.style.max_height(v);
204 self
205 }
206
207 pub fn flex(mut self, weight: f32) -> Self {
208 self.style = self.style.flex(weight);
209 self
210 }
211
212 pub fn expand(self) -> Self {
213 self.flex(1.0)
214 }
215
216 pub fn fill_width(mut self) -> Self {
217 self.style = self.style.width(Length::Fill(1.0));
218 self
219 }
220
221 pub fn fill_height(mut self) -> Self {
222 self.style = self.style.height(Length::Fill(1.0));
223 self
224 }
225
226 pub fn fill_size(self) -> Self {
227 self.fill_width().fill_height()
228 }
229
230 pub fn padding(mut self, padding: impl Into<Edges>) -> Self {
231 self.style = self.style.padding(padding);
232 self
233 }
234
235 pub fn pad(self, padding: impl Into<Edges>) -> Self {
236 self.padding(padding)
237 }
238
239 pub fn padding_xy(mut self, x: f32, y: f32) -> Self {
240 self.style = self.style.padding_xy(x, y);
241 self
242 }
243
244 fn resolved(&self, theme_size: f32) -> (f32, f32) {
245 (
246 self.font_size.unwrap_or(theme_size) * self.font_scale,
247 self.wrap_width.unwrap_or(0.0),
248 )
249 }
250}
251
252impl View for RichText {
253 fn body(&self) -> impl View + 'static {
254 let _ = (self.spans)();
255 Unit
256 }
257
258 fn is_leaf(&self) -> bool {
259 true
260 }
261
262 fn clip_self(&self) -> bool {
263 true
264 }
265
266 fn cursor_icon(&self, _state: sequoia_style::VisualState) -> CursorIcon {
267 if self.on_span_click.is_some() {
268 CursorIcon::Pointer
269 } else {
270 CursorIcon::Default
271 }
272 }
273
274 fn layout_style(&self) -> LayoutStyle {
275 self.style
276 }
277
278 fn intrinsic_size(&self, ctx: &mut LayoutCtx) -> Option<Size> {
279 let (size, _wrap) = self.resolved(ctx.default_font_size());
280 let spans = (self.spans)();
281 let pad = self.style.padding;
282 let auto_wrap = if self.auto_wrap {
283 text_wrap_limit(self.style, ctx.available_width())
284 } else {
285 None
286 };
287 let mut width = self.wrap_width.or(auto_wrap).unwrap_or(0.0);
288 if self.max_lines.is_some() && width <= 0.0 {
289 width = ctx
290 .rich_size_lh(
291 &spans,
292 size,
293 0.0,
294 self.wrap_mode,
295 self.letter_spacing,
296 self.align,
297 self.line_height,
298 )
299 .width
300 .max(1.0);
301 }
302 let shown = match self.max_lines {
303 Some(max_lines) => ctx.rich_elide_lines(
304 &spans,
305 size,
306 width,
307 self.wrap_mode,
308 self.letter_spacing,
309 self.align,
310 self.line_height,
311 max_lines,
312 ),
313 None => spans,
314 };
315 let text = ctx.rich_size_lh(
316 &shown,
317 size,
318 width,
319 self.wrap_mode,
320 self.letter_spacing,
321 self.align,
322 self.line_height,
323 );
324 Some(Size::new(
325 text.width + pad.horizontal_sum(),
326 text.height + pad.vertical_sum(),
327 ))
328 }
329
330 fn baseline_offset(&self, ctx: &mut LayoutCtx, width: f32) -> Option<f32> {
331 let (size, wrap) = self.resolved(ctx.default_font_size());
332 let spans = (self.spans)();
333 let pad = self.style.padding;
334 let width = if wrap > 0.0 {
335 wrap
336 } else {
337 (width - pad.horizontal_sum()).max(0.0)
338 };
339 let baseline = ctx.rich_first_baseline(
340 &spans,
341 size,
342 width,
343 self.wrap_mode,
344 self.letter_spacing,
345 self.align,
346 self.line_height,
347 );
348 Some(pad.top + baseline)
349 }
350
351 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
352 if ctx.phase != EventPhase::Target {
353 return;
354 }
355 let Some(handler) = &self.on_span_click else {
356 return;
357 };
358 if let Event::Pointer(PointerEvent::Down {
359 button: PointerButton::Left,
360 pos,
361 ..
362 }) = event
363 {
364 let size = self.font_size.unwrap_or_else(|| {
365 ctx.environment
366 .get::<sequoia_style::Theme>()
367 .map(|t| t.font_size)
368 .unwrap_or(14.0)
369 }) * self.font_scale;
370 let spans = (self.spans)();
371 let pad = self.style.padding;
372 let wrap = self.wrap_width.unwrap_or_else(|| {
373 if self.auto_wrap {
374 (ctx.rect.size.width - pad.horizontal_sum()).max(0.0)
375 } else {
376 0.0
377 }
378 });
379 let shown = match self.max_lines {
380 Some(max_lines) => ctx.text.elide_rich_lines(
381 &spans,
382 size,
383 wrap,
384 self.wrap_mode,
385 self.letter_spacing,
386 self.align,
387 self.line_height,
388 max_lines,
389 ),
390 None => spans,
391 };
392 let rich = ctx.text.shape_rich_full_lh(
393 &shown,
394 size,
395 wrap,
396 self.wrap_mode,
397 self.letter_spacing,
398 self.align,
399 self.line_height,
400 );
401 let text_h = rich.height();
402 let first_baseline = rich.line(0).map(|line| line.baseline).unwrap_or(0.0);
403 let block_top = match self.align_y {
404 TextVAlign::Baseline => pad.top - first_baseline,
405 TextVAlign::Top => pad.top,
406 TextVAlign::Center => {
407 pad.top + (ctx.rect.size.height - pad.vertical_sum() - text_h) * 0.5
408 }
409 TextVAlign::Bottom => {
410 pad.top + (ctx.rect.size.height - pad.vertical_sum() - text_h)
411 }
412 _ => pad.top,
413 };
414 let hit = rich.hit_point(
415 pos.x - ctx.rect.left() - pad.left,
416 pos.y - ctx.rect.top() - block_top,
417 );
418 if let Some(span) = hit.span {
419 handler(span);
420 ctx.handled();
421 }
422 }
423 }
424
425 fn paint_self(&self, ctx: &mut PaintCtx) {
426 let theme = ctx.theme();
427 let (size, wrap) = self.resolved(theme.font_size);
428 let color = self.color.unwrap_or(theme.text);
429 let spans = (self.spans)();
430 if spans.iter().all(|s| s.text.is_empty()) {
431 return;
432 }
433 let pad = self.style.padding;
434 let content_w = (ctx.rect.size.width - pad.horizontal_sum()).max(0.0);
435 let wrap = if wrap > 0.0 {
436 wrap
437 } else if self.auto_wrap {
438 content_w
439 } else {
440 0.0
441 };
442 let shown = match self.max_lines {
443 Some(max_lines) => ctx.elide_rich_lines(
444 &spans,
445 size,
446 wrap,
447 self.wrap_mode,
448 self.letter_spacing,
449 self.align,
450 self.line_height,
451 max_lines,
452 ),
453 None => spans,
454 };
455 let rich = ctx.shape_rich_full_lh(
456 &shown,
457 size,
458 wrap,
459 self.wrap_mode,
460 self.letter_spacing,
461 self.align,
462 self.line_height,
463 );
464 let text_h = rich.height();
465 let first_baseline = rich.line(0).map(|line| line.baseline).unwrap_or(0.0);
466 let block_top = match self.align_y {
467 TextVAlign::Baseline => ctx.rect.top() + pad.top - first_baseline,
468 TextVAlign::Top => ctx.rect.top() + pad.top,
469 TextVAlign::Center => {
470 ctx.rect.top()
471 + pad.top
472 + (ctx.rect.size.height - pad.vertical_sum() - text_h) * 0.5
473 }
474 TextVAlign::Bottom => {
475 ctx.rect.top() + pad.top + (ctx.rect.size.height - pad.vertical_sum() - text_h)
476 }
477 _ => ctx.rect.top() + pad.top,
478 };
479 ctx.draw_rich(&rich, (ctx.rect.left() + pad.left, block_top), color);
480 }
481}