Skip to main content

iced_selection/
markdown.rs

1//! A custom markdown viewer and its corresponding functions.
2use 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
21/// Display a bunch of markdown items.
22pub 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}
41/// Displays a heading using the default look.
42pub 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
89/// Displays a paragraph using the default look.
90pub 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
107/// Displays an unordered list using the default look and
108/// calling the [`Viewer`] for each bullet point item.
109///
110/// [`Viewer`]: https://docs.iced.rs/iced/widget/markdown/trait.Viewer.html
111pub 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
151/// Displays an ordered list using the default look and
152/// calling the [`Viewer`] for each numbered item.
153///
154/// [`Viewer`]: https://docs.iced.rs/iced/widget/markdown/trait.Viewer.html
155pub 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
190/// Displays a code block using the default look.
191pub 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
228/// The default [`Viewer`].
229pub 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    /// Creates a new [`SelectableViewer`] with the given `Theme`.
236    pub fn new(theme: Theme) -> Self {
237        Self {
238            theme,
239            highlighter: None,
240        }
241    }
242
243    /// Sets a custom [`text::Highlighter`] for the [`SelectableViewer`].
244    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}