1use omp_core::Str;
2
3use super::text::{append, paint_rich, truncate_rich};
4use crate::{
5 component::{Cached, Component, IntoChildren, MemoKey, PaintCtx, Slot, next_slot},
6 frame::Rect,
7 markdown::MdTheme,
8 props::{Prop, PropValue, Props},
9 rich::{Measure, RichText},
10};
11
12pub struct Markdown {
14 props: Props,
15 slot: Slot,
16 text: Str,
17 source: Str,
18 rich: RichText,
19 embedded: Vec<Cached>,
20 version: u64,
21 cached_width: u16,
22 cached: Option<MemoKey>,
23}
24
25impl Markdown {
26 pub fn new() -> Self {
28 Self {
29 props: Props::new(),
30 slot: next_slot(),
31 text: Str::default(),
32 source: Str::default(),
33 rich: RichText::default(),
34 embedded: Vec::new(),
35 version: 1,
36 cached_width: 0,
37 cached: None,
38 }
39 }
40
41 pub fn text_of(text: impl Into<Str>) -> Self {
43 Self::new().text(text)
44 }
45
46 pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
48 self.props.set(prop, value);
49 self.version = self.version.wrapping_add(1);
50 self
51 }
52
53 pub fn with_str(self, prop: Prop, value: &str) -> Self {
55 self.with(prop, value)
56 }
57
58 pub fn text(mut self, text: impl Into<Str>) -> Self {
60 let text = text.into();
61 append(&mut self.source, text.clone());
62 append(&mut self.text, text);
63 self.version = self.version.wrapping_add(1);
64 self
65 }
66
67 pub fn child(mut self, child: impl IntoChildren) -> Self {
69 child.extend_children(&mut self.embedded);
70 self
71 }
72
73 fn theme(&self, ctx: &crate::UiContext) -> MdTheme {
74 MdTheme::from_context(ctx).cascade(self.props.style(&ctx.theme))
75 }
76
77 fn render(&mut self, ctx: &crate::UiContext, width: u16) {
78 let width = width.max(1);
79 let key = MemoKey::new(self.version, ctx);
80 if self.cached_width == width && self.cached == Some(key) {
81 return;
82 }
83 let theme = self.theme(ctx);
84 let style = self.props.style(&ctx.theme);
85 self.rich.clear();
86 crate::markdown::render(&self.text, width, &theme, &mut self.rich);
87 truncate_rich(&mut self.rich, width, style, self.props.truncate());
88 self.cached_width = width;
89 self.cached = Some(key);
90 }
91}
92
93impl Default for Markdown {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99impl Component for Markdown {
100 fn props(&self) -> &Props {
101 &self.props
102 }
103
104 fn props_mut(&mut self) -> &mut Props {
105 &mut self.props
106 }
107
108 fn slot(&self) -> Slot {
109 self.slot
110 }
111
112 fn children(&self) -> &[Cached] {
113 &self.embedded
114 }
115
116 fn children_mut(&mut self) -> &mut [Cached] {
117 &mut self.embedded
118 }
119
120 fn measure(&mut self, ctx: &crate::UiContext) -> (u16, u16) {
121 let theme = self.theme(ctx);
122 let mut natural = Measure::default();
123 crate::markdown::render(&self.text, u16::MAX, &theme, &mut natural);
124 let mut min = natural.widest.clamp(1, 12);
125 let mut nat = natural.widest.max(min);
126 for child in &mut self.embedded {
127 if child.visible {
128 let (child_min, child_nat) = child.measure(ctx);
129 min = min.max(child_min);
130 nat = nat.max(child_nat);
131 }
132 }
133 (min, nat)
134 }
135
136 fn height(&mut self, ctx: &crate::UiContext, width: u16) -> u16 {
137 self.render(ctx, width);
138 let mut height = if self.text.is_empty() {
139 0
140 } else {
141 RichText::rows(&self.rich)
142 };
143 let mut placed = !self.text.is_empty();
144 for child in &mut self.embedded {
145 if !child.visible {
146 continue;
147 }
148 if placed {
149 height = height.saturating_add(1);
150 }
151 height = height.saturating_add(child.height(ctx, width));
152 placed = true;
153 }
154 height
155 }
156
157 fn place(&mut self, ctx: &crate::UiContext, content: Rect) {
158 self.render(ctx, content.width);
159 let mut cursor = content.y;
160 let mut placed = if self.text.is_empty() {
161 false
162 } else {
163 cursor = cursor.saturating_add(RichText::rows(&self.rich));
164 true
165 };
166 for child in &mut self.embedded {
167 if !child.visible {
168 continue;
169 }
170 if placed {
171 cursor = cursor.saturating_add(1);
172 }
173 let height = child.height(ctx, content.width);
174 child.place(ctx, Rect::new(content.x, cursor, content.width, height));
175 cursor = cursor.saturating_add(height);
176 placed = true;
177 }
178 }
179
180 fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
181 if !self.text.is_empty() {
182 self.render(pc.ctx, rect.width);
183 let own = Rect::new(rect.x, rect.y, rect.width, RichText::rows(&self.rich));
184 paint_rich(pc, own, &self.rich, self.props.align());
185 }
186 for child in &mut self.embedded {
187 if child.visible {
188 child.paint(pc);
189 }
190 }
191 }
192
193 fn set_text(&mut self, ctx: &crate::UiContext, text: Str) -> bool {
194 if self.source == text {
195 return false;
196 }
197 self.source = text.clone();
198 if crate::markup::md_embeds_markup(&text) {
199 if let Ok(children) = crate::markup::parse_md_fragment_inheriting(&text, ctx, &self.props)
200 {
201 self.text = Str::default();
202 self.embedded = children;
203 } else {
204 self.text = text;
205 self.embedded.clear();
206 }
207 } else {
208 self.text = text;
209 self.embedded.clear();
210 }
211 self.version = self.version.wrapping_add(1);
212 true
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::context::UiContext;
220
221 #[test]
222 fn set_text_regrafts_static_markup_in_document_order() {
223 let ctx = UiContext::default();
224 let mut markdown = Markdown::text_of("old");
225 assert!(markdown.set_text(&ctx, Str::new("before\n<box><text>inside</text></box>\nafter"),));
226 assert!(markdown.text.is_empty());
227 assert_eq!(markdown.embedded.len(), 3);
228 }
229
230 #[test]
231 fn set_text_degrades_rejected_dynamic_markup_to_literal_text() {
232 let ctx = UiContext::default();
233 for source in ["<input/>", "<box id=duplicate/>", "<box when=\"x == y\"/>", "</md>"] {
234 let mut markdown = Markdown::text_of("old");
235 assert!(markdown.set_text(&ctx, Str::new(source)));
236 assert_eq!(markdown.text, source);
237 assert!(markdown.embedded.is_empty());
238 }
239 }
240}