Skip to main content

sequoia_view/
rich_text.rs

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}