iced_selection/
markdown.rs

1//! A custom markdown viewer and its corresponding functions.
2use iced_widget::graphics::text::Paragraph;
3use iced_widget::markdown::{
4    Bullet, Catalog, HeadingLevel, Item, Settings, Text, Viewer, view_with,
5};
6pub use iced_widget::markdown::{Content, Uri, parse};
7use iced_widget::{checkbox, column, container, row, scrollable};
8
9use crate::core::Font;
10use crate::core::alignment;
11use crate::core::padding;
12use crate::core::{self, Element, Length, Pixels};
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: impl IntoIterator<Item = &'a Item>,
24    settings: impl Into<Settings>,
25) -> Element<'a, Uri, Theme, Renderer>
26where
27    Theme: Catalog + text::Catalog + 'a,
28    Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
29{
30    view_with(items, settings, &SelectableViewer)
31}
32/// Displays a heading using the default look.
33pub fn heading<'a, Message, Theme, Renderer>(
34    settings: Settings,
35    level: &'a HeadingLevel,
36    text: &'a Text,
37    index: usize,
38    on_link_click: impl Fn(Uri) -> Message + 'a,
39) -> Element<'a, Message, Theme, Renderer>
40where
41    Message: 'a,
42    Theme: Catalog + text::Catalog + 'a,
43    Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
44{
45    let Settings {
46        h1_size,
47        h2_size,
48        h3_size,
49        h4_size,
50        h5_size,
51        h6_size,
52        text_size,
53        ..
54    } = settings;
55
56    container(
57        rich_text(text.spans(settings.style))
58            .on_link_click(on_link_click)
59            .size(match level {
60                HeadingLevel::H1 => h1_size,
61                HeadingLevel::H2 => h2_size,
62                HeadingLevel::H3 => h3_size,
63                HeadingLevel::H4 => h4_size,
64                HeadingLevel::H5 => h5_size,
65                HeadingLevel::H6 => h6_size,
66            }),
67    )
68    .padding(padding::top(if index > 0 {
69        text_size / 2.0
70    } else {
71        Pixels::ZERO
72    }))
73    .into()
74}
75
76/// Displays a paragraph using the default look.
77pub fn paragraph<'a, Message, Theme, Renderer>(
78    settings: Settings,
79    text: &Text,
80    on_link_click: impl Fn(Uri) -> Message + 'a,
81) -> Element<'a, Message, Theme, Renderer>
82where
83    Message: 'a,
84    Theme: Catalog + text::Catalog + 'a,
85    Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
86{
87    rich_text(text.spans(settings.style))
88        .size(settings.text_size)
89        .on_link_click(on_link_click)
90        .into()
91}
92
93/// Displays an unordered list using the default look and
94/// calling the [`Viewer`] for each bullet point item.
95///
96/// [`Viewer`]: https://docs.iced.rs/iced/widget/markdown/trait.Viewer.html
97pub fn unordered_list<'a, Message, Theme, Renderer>(
98    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
99    settings: Settings,
100    bullets: &'a [Bullet],
101) -> Element<'a, Message, Theme, Renderer>
102where
103    Message: 'a,
104    Theme: Catalog + text::Catalog + 'a,
105    Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
106{
107    column(bullets.iter().map(|bullet| {
108        row![
109            match bullet {
110                Bullet::Point { .. } => {
111                    text("•").size(settings.text_size).into()
112                }
113                Bullet::Task { done, .. } => {
114                    Element::from(
115                        container(checkbox(*done).size(settings.text_size))
116                            .center_y(
117                                iced_widget::text::LineHeight::default()
118                                    .to_absolute(settings.text_size),
119                            ),
120                    )
121                }
122            },
123            view_with(
124                bullet_items(bullet),
125                Settings {
126                    spacing: settings.spacing * 0.6,
127                    ..settings
128                },
129                viewer,
130            )
131        ]
132        .spacing(settings.spacing)
133        .into()
134    }))
135    .spacing(settings.spacing * 0.75)
136    .padding([0.0, settings.spacing.0])
137    .into()
138}
139
140/// Displays an ordered list using the default look and
141/// calling the [`Viewer`] for each numbered item.
142///
143/// [`Viewer`]: https://docs.iced.rs/iced/widget/markdown/trait.Viewer.html
144pub fn ordered_list<'a, Message, Theme, Renderer>(
145    viewer: &impl Viewer<'a, Message, Theme, Renderer>,
146    settings: Settings,
147    start: u64,
148    bullets: &'a [Bullet],
149) -> Element<'a, Message, Theme, Renderer>
150where
151    Message: 'a,
152    Theme: Catalog + text::Catalog + 'a,
153    Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
154{
155    let digits = ((start + bullets.len() as u64).max(1) as f32)
156        .log10()
157        .ceil();
158
159    column(bullets.iter().enumerate().map(|(i, bullet)| {
160        row![
161            text!("{}.", i as u64 + start)
162                .size(settings.text_size)
163                .align_x(alignment::Horizontal::Right)
164                .width(settings.text_size * ((digits / 2.0).ceil() + 1.0)),
165            view_with(
166                bullet_items(bullet),
167                Settings {
168                    spacing: settings.spacing * 0.6,
169                    ..settings
170                },
171                viewer,
172            )
173        ]
174        .spacing(settings.spacing)
175        .into()
176    }))
177    .spacing(settings.spacing * 0.75)
178    .into()
179}
180
181/// Displays a code block using the default look.
182pub fn code_block<'a, Message, Theme, Renderer>(
183    settings: Settings,
184    lines: &'a [Text],
185    on_link_click: impl Fn(Uri) -> Message + Clone + 'a,
186) -> Element<'a, Message, Theme, Renderer>
187where
188    Message: 'a,
189    Theme: Catalog + text::Catalog + 'a,
190    Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
191{
192    container(
193        scrollable(
194            container(column(lines.iter().map(|line| {
195                rich_text(line.spans(settings.style))
196                    .on_link_click(on_link_click.clone())
197                    .font(Font::MONOSPACE)
198                    .size(settings.code_size)
199                    .into()
200            })))
201            .padding(settings.code_size),
202        )
203        .direction(scrollable::Direction::Horizontal(
204            scrollable::Scrollbar::default()
205                .width(settings.code_size / 2)
206                .scroller_width(settings.code_size / 2),
207        )),
208    )
209    .width(Length::Fill)
210    .padding(settings.code_size / 4)
211    .class(Theme::code_block())
212    .into()
213}
214
215#[derive(Debug, Clone, Copy)]
216struct SelectableViewer;
217
218impl<'a, Theme, Renderer> Viewer<'a, Uri, Theme, Renderer> for SelectableViewer
219where
220    Theme: Catalog + text::Catalog + 'a,
221    Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
222{
223    fn on_link_click(url: Uri) -> Uri {
224        url
225    }
226
227    fn heading(
228        &self,
229        settings: Settings,
230        level: &'a HeadingLevel,
231        text: &'a Text,
232        index: usize,
233    ) -> Element<'a, Uri, Theme, Renderer> {
234        heading::<'a, Uri, Theme, Renderer>(
235            settings,
236            level,
237            text,
238            index,
239            |url| url,
240        )
241    }
242
243    fn paragraph(
244        &self,
245        settings: Settings,
246        text: &Text,
247    ) -> Element<'a, Uri, Theme, Renderer> {
248        paragraph(settings, text, |url| url)
249    }
250
251    fn unordered_list(
252        &self,
253        settings: Settings,
254        items: &'a [Bullet],
255    ) -> Element<'a, Uri, Theme, Renderer> {
256        unordered_list(self, settings, items)
257    }
258
259    fn ordered_list(
260        &self,
261        settings: Settings,
262        start: u64,
263        items: &'a [Bullet],
264    ) -> Element<'a, Uri, Theme, Renderer> {
265        ordered_list(self, settings, start, items)
266    }
267
268    fn code_block(
269        &self,
270        settings: Settings,
271        _language: Option<&'a str>,
272        _code: &'a str,
273        lines: &'a [Text],
274    ) -> Element<'a, Uri, Theme, Renderer> {
275        code_block(settings, lines, |url| url)
276    }
277}