1use iced_widget::core::font;
3use iced_widget::graphics::text::Paragraph;
4use iced_widget::markdown::{
5 Bullet, Catalog, HeadingLevel, Item, Text, Viewer, view_with,
6};
7pub use iced_widget::markdown::{Content, Settings, Uri, parse};
8use iced_widget::{Widget, checkbox, column, container, row, scrollable};
9
10use crate::core::alignment;
11use crate::core::padding;
12use crate::core::{self, Code, Font, Length};
13use crate::{rich_text, text};
14
15fn bullet_items(bullet: &Bullet) -> &[Item] {
16 match bullet {
17 Bullet::Point { items } | Bullet::Task { items, .. } => items,
18 }
19}
20
21pub fn view<'a, Theme, Renderer>(
23 items: &'a [Item],
24 settings: impl Into<Settings>,
25 theme: Theme,
26) -> impl Widget<Uri, Theme, Renderer> + 'a
27where
28 Theme: Catalog + text::Catalog + 'a,
29 Renderer: core::text::Renderer<Paragraph = Paragraph> + 'a,
30{
31 view_with(
32 items,
33 settings,
34 &SelectableViewer {
35 theme,
36 highlighter: None,
37 },
38 )
39 .boxed()
40}
41pub fn heading<'a, Message, Theme, Renderer>(
43 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
44 settings: Settings,
45 level: &'a HeadingLevel,
46 text: &'a Text,
47 on_link_click: impl Fn(Uri) -> Message + 'a,
48) -> impl Widget<Message, Theme, Renderer> + 'a
49where
50 Message: 'a,
51 Theme: Catalog + text::Catalog + 'a,
52 Renderer: core::text::Renderer<Paragraph = Paragraph> + 'a,
53{
54 let Settings {
55 h1_size,
56 h2_size,
57 h3_size,
58 h4_size,
59 h5_size,
60 h6_size,
61 ..
62 } = settings;
63
64 container(
65 rich_text(text.spans(
66 Settings {
67 font: Font {
68 weight: font::Weight::Bold,
69 ..settings.font
70 },
71 ..settings
72 },
73 viewer.theme(),
74 viewer.highlighter(),
75 ))
76 .on_link_click(on_link_click)
77 .size(match level {
78 HeadingLevel::H1 => h1_size,
79 HeadingLevel::H2 => h2_size,
80 HeadingLevel::H3 => h3_size,
81 HeadingLevel::H4 => h4_size,
82 HeadingLevel::H5 => h5_size,
83 HeadingLevel::H6 => h6_size,
84 })
85 .line_height(settings.line_height),
86 )
87}
88
89pub fn paragraph<'a, Message, Theme, Renderer>(
91 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
92 settings: Settings,
93 text: &Text,
94 on_link_click: impl Fn(Uri) -> Message + 'a,
95) -> impl Widget<Message, Theme, Renderer> + 'a
96where
97 Message: 'a,
98 Theme: Catalog + text::Catalog + 'a,
99 Renderer: core::text::Renderer<Paragraph = Paragraph> + 'a,
100{
101 rich_text(text.spans(settings, viewer.theme(), viewer.highlighter()))
102 .size(settings.text_size)
103 .line_height(settings.line_height)
104 .on_link_click(on_link_click)
105}
106
107pub fn unordered_list<'a, Message, Theme, Renderer>(
112 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
113 settings: Settings,
114 bullets: &'a [Bullet],
115) -> impl Widget<Message, Theme, Renderer> + 'a
116where
117 Message: 'a,
118 Theme: Catalog + text::Catalog + 'a,
119 Renderer: core::text::Renderer<Paragraph = Paragraph> + 'a,
120{
121 column(bullets.iter().map(|bullet| {
122 row![
123 match bullet {
124 Bullet::Point { .. } => {
125 text("•").size(settings.text_size).boxed()
126 }
127 Bullet::Task { done, .. } => {
128 container(checkbox(*done).size(settings.text_size))
129 .center_y(
130 iced_widget::text::LineHeight::default()
131 .to_absolute(settings.text_size),
132 )
133 .boxed()
134 }
135 },
136 view_with(
137 bullet_items(bullet),
138 Settings {
139 spacing: settings.spacing / 2.0,
140 ..settings
141 },
142 viewer,
143 )
144 ]
145 .spacing(settings.text_size / 2.0)
146 }))
147 .spacing(settings.spacing / 2.0)
148 .padding(padding::left(settings.text_size.0))
149}
150
151pub fn ordered_list<'a, Message, Theme, Renderer>(
156 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
157 settings: Settings,
158 start: u64,
159 bullets: &'a [Bullet],
160) -> impl Widget<Message, Theme, Renderer> + 'a
161where
162 Message: 'a,
163 Theme: Catalog + text::Catalog + 'a,
164 Renderer: core::text::Renderer<Paragraph = Paragraph> + 'a,
165{
166 let digits = (start + bullets.len() as u64).max(1).ilog10() + 1;
167
168 column(bullets.iter().enumerate().map(|(i, bullet)| {
169 row![
170 text!("{}.", i as u64 + start)
171 .size(settings.text_size)
172 .align_x(alignment::Horizontal::Right)
173 .width(
174 settings.text_size * ((digits as f32 / 2.0).ceil() + 1.0)
175 ),
176 view_with(
177 bullet_items(bullet),
178 Settings {
179 spacing: settings.spacing / 2.0,
180 ..settings
181 },
182 viewer,
183 )
184 ]
185 .spacing(settings.text_size / 2.0)
186 }))
187 .spacing(settings.spacing / 2.0)
188}
189
190pub fn code_block<'a, Message, Theme, Renderer>(
192 viewer: &impl Viewer<'a, Message, Theme, Renderer>,
193 settings: Settings,
194 lines: &'a [Text],
195 on_link_click: impl Fn(Uri) -> Message + Clone + 'a,
196) -> impl Widget<Message, Theme, Renderer> + 'a
197where
198 Message: 'a,
199 Theme: Catalog + text::Catalog + 'a,
200 Renderer: core::text::Renderer<Paragraph = Paragraph> + 'a,
201{
202 let padding = settings.code_block_size / 0.85 * 0.75;
203
204 container(
205 scrollable(column(lines.iter().map(|line| {
206 rich_text(line.spans(
207 settings,
208 viewer.theme(),
209 viewer.highlighter(),
210 ))
211 .on_link_click(on_link_click.clone())
212 .font(settings.code_block_font)
213 .size(settings.code_block_size)
214 .line_height(settings.line_height)
215 })))
216 .direction(scrollable::Direction::Horizontal(
217 scrollable::Scrollbar::default()
218 .width(padding / 2.0)
219 .scroller_width(padding / 2.0),
220 ))
221 .spacing(padding),
222 )
223 .width(Length::Fill)
224 .padding(padding)
225 .class(Theme::code_block())
226}
227
228pub struct SelectableViewer<'a, Theme> {
230 theme: Theme,
231 highlighter: Option<Box<dyn text::Highlighter<Code, Theme> + 'a>>,
232}
233
234impl<'a, Theme> SelectableViewer<'a, Theme> {
235 pub fn new(theme: Theme) -> Self {
237 Self {
238 theme,
239 highlighter: None,
240 }
241 }
242
243 pub fn highlighter(
245 mut self,
246 highlighter: impl text::Highlighter<Code, Theme> + 'a,
247 ) -> Self {
248 self.highlighter = Some(Box::new(highlighter));
249 self
250 }
251}
252
253impl<'a, Theme, Renderer> Viewer<'a, Uri, Theme, Renderer>
254 for SelectableViewer<'a, Theme>
255where
256 Theme: Catalog + text::Catalog + 'a,
257 Renderer: core::text::Renderer<Paragraph = Paragraph> + 'a,
258{
259 fn theme(&self) -> &Theme {
260 &self.theme
261 }
262
263 fn highlighter(&self) -> &dyn text::Highlighter<Code, Theme> {
264 self.highlighter
265 .as_deref()
266 .unwrap_or_else(|| self.theme.highlighter())
267 }
268
269 fn on_link_click(url: Uri) -> Uri {
270 url
271 }
272
273 fn heading(
274 &self,
275 settings: Settings,
276 level: &'a HeadingLevel,
277 text: &'a Text,
278 ) -> impl Widget<Uri, Theme, Renderer> + 'a {
279 heading(self, settings, level, text, |url| url)
280 }
281
282 fn paragraph(
283 &self,
284 settings: Settings,
285 text: &Text,
286 ) -> impl Widget<Uri, Theme, Renderer> + 'a {
287 paragraph(self, settings, text, |url| url)
288 }
289
290 fn unordered_list(
291 &self,
292 settings: Settings,
293 items: &'a [Bullet],
294 ) -> impl Widget<Uri, Theme, Renderer> + 'a {
295 unordered_list(self, settings, items)
296 }
297
298 fn ordered_list(
299 &self,
300 settings: Settings,
301 start: u64,
302 items: &'a [Bullet],
303 ) -> impl Widget<Uri, Theme, Renderer> + 'a {
304 ordered_list(self, settings, start, items)
305 }
306
307 fn code_block(
308 &self,
309 settings: Settings,
310 _language: Option<&'a str>,
311 _code: &'a str,
312 lines: &'a [Text],
313 ) -> impl Widget<Uri, Theme, Renderer> + 'a {
314 code_block(self, settings, lines, |url| url)
315 }
316}