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, RichAlign, TextSpan, WrapMode};
8
9use crate::view::{EventCtx, PaintCtx, View};
10use crate::views::Unit;
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 letter_spacing: f32,
20 on_span_click: Option<Rc<dyn Fn(usize)>>,
21 style: LayoutStyle,
22}
23
24impl RichText {
25 pub fn new(spans: Vec<TextSpan>) -> Self {
26 Self {
27 spans: Box::new(move || spans.clone()),
28 font_size: None,
29 color: None,
30 wrap_width: None,
31 wrap_mode: WrapMode::default(),
32 align: RichAlign::Left,
33 letter_spacing: 0.0,
34 on_span_click: None,
35 style: LayoutStyle {
36 width: Length::Fit,
37 height: Length::Fit,
38 ..LayoutStyle::default()
39 },
40 }
41 }
42
43 pub fn with(spans: impl Fn() -> Vec<TextSpan> + 'static) -> Self {
44 Self {
45 spans: Box::new(spans),
46 font_size: None,
47 color: None,
48 wrap_width: None,
49 wrap_mode: WrapMode::default(),
50 align: RichAlign::Left,
51 letter_spacing: 0.0,
52 on_span_click: None,
53 style: LayoutStyle {
54 width: Length::Fit,
55 height: Length::Fit,
56 ..LayoutStyle::default()
57 },
58 }
59 }
60
61 pub fn spans(&self) -> Vec<TextSpan> {
62 (self.spans)()
63 }
64
65 pub fn span(mut self, span: TextSpan) -> Self {
66 let prev = std::mem::replace(&mut self.spans, Box::new(Vec::new));
67 self.spans = Box::new(move || {
68 let mut out = prev();
69 out.push(span.clone());
70 out
71 });
72 self
73 }
74
75 pub fn size(mut self, size: f32) -> Self {
76 self.font_size = Some(size);
77 self
78 }
79
80 pub fn color(mut self, color: Color) -> Self {
81 self.color = Some(color);
82 self
83 }
84
85 pub fn wrap_width(mut self, width: f32) -> Self {
86 self.wrap_width = if width > 0.0 { Some(width) } else { None };
87 self
88 }
89
90 pub fn wrap_mode(mut self, mode: WrapMode) -> Self {
91 self.wrap_mode = mode;
92 self
93 }
94
95 pub fn align(mut self, align: RichAlign) -> Self {
96 self.align = align;
97 self
98 }
99
100 pub fn centered(mut self) -> Self {
101 self.align = RichAlign::Center;
102 self
103 }
104
105 pub fn right_aligned(mut self) -> Self {
106 self.align = RichAlign::Right;
107 self
108 }
109
110 pub fn letter_spacing(mut self, spacing: LetterSpacing) -> Self {
111 self.letter_spacing = if spacing.is_finite() { spacing } else { 0.0 };
112 self
113 }
114
115 pub fn on_span_click(mut self, handler: impl Fn(usize) + 'static) -> Self {
116 self.on_span_click = Some(Rc::new(handler));
117 self
118 }
119
120 pub fn width(mut self, width: impl Into<Length>) -> Self {
121 self.style = self.style.width(width.into());
122 self
123 }
124
125 pub fn height(mut self, height: impl Into<Length>) -> Self {
126 self.style = self.style.height(height.into());
127 self
128 }
129
130 pub fn min_width(mut self, v: f32) -> Self {
131 self.style = self.style.min_width(v);
132 self
133 }
134
135 pub fn max_width(mut self, v: f32) -> Self {
136 self.style = self.style.max_width(v);
137 self
138 }
139
140 pub fn min_height(mut self, v: f32) -> Self {
141 self.style = self.style.min_height(v);
142 self
143 }
144
145 pub fn max_height(mut self, v: f32) -> Self {
146 self.style = self.style.max_height(v);
147 self
148 }
149
150 pub fn flex(mut self, weight: f32) -> Self {
151 self.style = self.style.flex(weight);
152 self
153 }
154
155 pub fn expand(self) -> Self {
156 self.flex(1.0)
157 }
158
159 pub fn fill_width(mut self) -> Self {
160 self.style = self.style.width(Length::Fill(1.0));
161 self
162 }
163
164 pub fn fill_height(mut self) -> Self {
165 self.style = self.style.height(Length::Fill(1.0));
166 self
167 }
168
169 pub fn fill_size(self) -> Self {
170 self.fill_width().fill_height()
171 }
172
173 pub fn padding(mut self, padding: impl Into<Edges>) -> Self {
174 self.style = self.style.padding(padding);
175 self
176 }
177
178 pub fn pad(self, padding: impl Into<Edges>) -> Self {
179 self.padding(padding)
180 }
181
182 pub fn padding_xy(mut self, x: f32, y: f32) -> Self {
183 self.style = self.style.padding_xy(x, y);
184 self
185 }
186
187 fn resolved(&self, theme_size: f32) -> (f32, f32) {
188 (
189 self.font_size.unwrap_or(theme_size),
190 self.wrap_width.unwrap_or(0.0),
191 )
192 }
193}
194
195impl View for RichText {
196 fn body(&self) -> impl View + 'static {
197 let _ = (self.spans)();
198 Unit
199 }
200
201 fn is_leaf(&self) -> bool {
202 true
203 }
204
205 fn clip_self(&self) -> bool {
206 true
207 }
208
209 fn cursor_icon(&self, _state: sequoia_style::VisualState) -> CursorIcon {
210 if self.on_span_click.is_some() {
211 CursorIcon::Pointer
212 } else {
213 CursorIcon::Default
214 }
215 }
216
217 fn layout_style(&self) -> LayoutStyle {
218 self.style
219 }
220
221 fn intrinsic_size(&self, ctx: &mut LayoutCtx) -> Option<Size> {
222 let (size, wrap) = self.resolved(14.0);
223 let spans = (self.spans)();
224 let pad = self.style.padding;
225 let text = ctx.rich_size(
226 &spans,
227 size,
228 wrap,
229 self.wrap_mode,
230 self.letter_spacing,
231 self.align,
232 );
233 Some(Size::new(
234 text.width + pad.horizontal_sum(),
235 text.height + pad.vertical_sum(),
236 ))
237 }
238
239 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
240 if ctx.phase != EventPhase::Target {
241 return;
242 }
243 let Some(handler) = &self.on_span_click else {
244 return;
245 };
246 if let Event::Pointer(PointerEvent::Down {
247 button: PointerButton::Left,
248 pos,
249 ..
250 }) = event
251 {
252 let size = self.font_size.unwrap_or_else(|| {
253 ctx.environment
254 .get::<sequoia_style::Theme>()
255 .map(|t| t.font_size)
256 .unwrap_or(14.0)
257 });
258 let wrap = self.wrap_width.unwrap_or(0.0);
259 let spans = (self.spans)();
260 let pad = self.style.padding;
261 let rich = ctx.shape_rich_full(
262 &spans,
263 size,
264 wrap,
265 self.wrap_mode,
266 self.letter_spacing,
267 self.align,
268 );
269 let hit = rich.hit_point(
270 pos.x - ctx.rect.left() - pad.left,
271 pos.y - ctx.rect.top() - pad.top,
272 );
273 if let Some(span) = hit.span {
274 handler(span);
275 ctx.handled();
276 }
277 }
278 }
279
280 fn paint_self(&self, ctx: &mut PaintCtx) {
281 let theme = ctx.theme();
282 let (size, wrap) = self.resolved(theme.font_size);
283 let color = self.color.unwrap_or(theme.text);
284 let spans = (self.spans)();
285 if spans.iter().all(|s| s.text.is_empty()) {
286 return;
287 }
288 let pad = self.style.padding;
289 let origin = (ctx.rect.left() + pad.left, ctx.rect.top() + pad.top);
290 ctx.draw_rich_spans(
291 &spans,
292 origin,
293 size,
294 wrap,
295 self.wrap_mode,
296 self.letter_spacing,
297 self.align,
298 color,
299 );
300 }
301}