makepad_widgets/
markdown.rs

1use crate::{
2    makepad_derive_widget::*,
3    makepad_draw::*,
4    widget::*,
5    text_flow::TextFlow,
6    link_label::LinkLabel,
7    WidgetMatchEvent,
8};
9
10use pulldown_cmark::{Event as MdEvent, HeadingLevel, Options, Parser, Tag, TagEnd};
11
12live_design!{
13    link widgets;
14    use link::theme::*;
15    use makepad_draw::shader::std::*;
16    use crate::link_label::LinkLabelBase;
17    
18    pub MarkdownLinkBase = {{MarkdownLink}}<LinkLabelBase> {
19        /*link = {
20            draw_text:{
21                // other blue hyperlink colors: #1a0dab, // #0969da  // #0c50d1
22                color: #1a0dab
23            }
24        }*/
25    }
26
27    pub MarkdownBase = {{Markdown}} {
28        // ok so we can use one drawtext
29        // change to italic, change bold (SDF), strikethrough
30    }
31    
32    pub MarkdownLink = <MarkdownLinkBase> {
33        width: Fit, height: Fit,
34        align: {x: 0., y: 0.}
35        
36        label_walk: { width: Fit, height: Fit }
37        
38        draw_icon: {
39            instance hover: 0.0
40            instance pressed: 0.0
41
42            fn get_color(self) -> vec4 {
43                return mix(
44                    mix(
45                        THEME_COLOR_LABEL_INNER,
46                        THEME_COLOR_LABEL_INNER_HOVER,
47                        self.hover
48                    ),
49                    THEME_COLOR_LABEL_INNER_DOWN,
50                    self.pressed
51                )
52            }
53        }
54        
55        animator: {
56            hover = {
57                default: off,
58                off = {
59                    from: {all: Forward {duration: 0.1}}
60                    apply: {
61                        draw_bg: {pressed: 0.0, hover: 0.0}
62                        draw_icon: {pressed: 0.0, hover: 0.0}
63                        draw_text: {pressed: 0.0, hover: 0.0}
64                    }
65                }
66                
67                on = {
68                    from: {
69                        all: Forward {duration: 0.1}
70                        pressed: Forward {duration: 0.01}
71                    }
72                    apply: {
73                        draw_bg: {pressed: 0.0, hover: [{time: 0.0, value: 1.0}],}
74                        draw_icon: {pressed: 0.0, hover: [{time: 0.0, value: 1.0}],}
75                        draw_text: {pressed: 0.0, hover: [{time: 0.0, value: 1.0}],}
76                    }
77                }
78                
79                pressed = {
80                    from: {all: Forward {duration: 0.2}}
81                    apply: {
82                        draw_bg: {pressed: [{time: 0.0, value: 1.0}], hover: 1.0,}
83                        draw_icon: {pressed: [{time: 0.0, value: 1.0}], hover: 1.0,}
84                        draw_text: {pressed: [{time: 0.0, value: 1.0}], hover: 1.0,}
85                    }
86                }
87            }
88        }
89        
90        draw_bg: {
91            instance pressed: 0.0
92            instance hover: 0.0
93
94            fn pixel(self) -> vec4 {
95                let sdf = Sdf2d::viewport(self.pos * self.rect_size);
96                let offset_y = 1.0
97                sdf.move_to(0., self.rect_size.y - offset_y);
98                sdf.line_to(self.rect_size.x, self.rect_size.y - offset_y);
99                return sdf.stroke(mix(
100                    THEME_COLOR_LABEL_INNER,
101                    THEME_COLOR_LABEL_INNER_DOWN,
102                    self.pressed
103                ), mix(0.0, 0.8, self.hover));
104            }
105        }
106        
107        draw_text: {
108            instance pressed: 0.0
109            instance hover: 0.0
110
111            uniform color_hover: (THEME_COLOR_LABEL_INNER_HOVER),
112            uniform color_pressed: (THEME_COLOR_LABEL_INNER_DOWN),
113
114            wrap: Word
115            color: (THEME_COLOR_LABEL_INNER),
116            text_style: <THEME_FONT_REGULAR>{
117                font_size: (THEME_FONT_SIZE_P)
118            }
119            fn get_color(self) -> vec4 {
120                return mix(
121                    mix(
122                        self.color,
123                        self.color_hover,
124                        self.hover
125                    ),
126                    self.color_pressed,
127                    self.pressed
128                )
129            }
130        }
131    }
132    
133    pub Markdown = <MarkdownBase> {
134        width:Fill, height:Fit,
135        flow: RightWrap,
136        padding: <THEME_MSPACE_1> {}
137                
138        font_size: (THEME_FONT_SIZE_P),
139        font_color: (THEME_COLOR_LABEL_INNER),
140        
141        paragraph_spacing: 16,
142        pre_code_spacing: 8,
143        inline_code_padding: <THEME_MSPACE_1> {},
144        inline_code_margin: <THEME_MSPACE_1> {},
145                
146        draw_normal: {
147            text_style: <THEME_FONT_REGULAR> {
148                font_size: (THEME_FONT_SIZE_P)
149            }
150            color: (THEME_COLOR_LABEL_INNER)
151        }
152        
153        draw_italic: {
154            text_style: <THEME_FONT_ITALIC> {
155                font_size: (THEME_FONT_SIZE_P)
156            }
157            color: (THEME_COLOR_LABEL_INNER)
158        }
159        
160        draw_bold: {
161            text_style: <THEME_FONT_BOLD> {
162                font_size: (THEME_FONT_SIZE_P)
163            }
164            color: (THEME_COLOR_LABEL_INNER)
165        }
166        
167        draw_bold_italic: {
168            text_style: <THEME_FONT_BOLD_ITALIC> {
169                font_size: (THEME_FONT_SIZE_P)
170            }
171            color: (THEME_COLOR_LABEL_INNER)
172        }
173        
174        draw_fixed: {
175            temp_y_shift: 0.25
176            text_style: <THEME_FONT_CODE> {
177                font_size: (THEME_FONT_SIZE_P)
178            }
179            color: (THEME_COLOR_LABEL_INNER)
180        }
181        
182        code_layout: {
183            flow: RightWrap,
184            padding: <THEME_MSPACE_2> { left: (THEME_SPACE_3), right: (THEME_SPACE_3), bottom:10 }
185        }
186        code_walk: { width: Fill, height: Fit }
187        
188        quote_layout: {
189            flow: RightWrap,
190            padding: <THEME_MSPACE_2> { left: (THEME_SPACE_3), right: (THEME_SPACE_3) }
191        }
192        quote_walk: { width: Fill, height: Fit, }
193        
194        list_item_layout: {
195            flow: RightWrap,
196            padding: <THEME_MSPACE_1> {}
197        }
198        list_item_walk: {
199            height: Fit, width: Fill,
200        }
201        
202        sep_walk: {
203            width: Fill, height: 4.
204            margin: <THEME_MSPACE_V_1> {}
205        }
206        
207        draw_block: {
208            line_color: (THEME_COLOR_LABEL_INNER)
209            sep_color: (THEME_COLOR_SHADOW)
210            quote_bg_color: (THEME_COLOR_BG_HIGHLIGHT)
211            quote_fg_color: (THEME_COLOR_LABEL_INNER)
212            code_color: (THEME_COLOR_BG_HIGHLIGHT)
213            
214            fn pixel(self) -> vec4 {
215                let sdf = Sdf2d::viewport(self.pos * self.rect_size);
216                match self.block_type {
217                    FlowBlockType::Quote => {
218                        sdf.box(
219                            0.,
220                            0.,
221                            self.rect_size.x,
222                            self.rect_size.y,
223                            2.
224                        );
225                        sdf.fill(self.quote_bg_color)
226                        sdf.box(
227                            THEME_SPACE_1,
228                            THEME_SPACE_1,
229                            THEME_SPACE_1,
230                            self.rect_size.y - THEME_SPACE_2,
231                            1.5
232                        );
233                        sdf.fill(self.quote_fg_color)
234                        return sdf.result;
235                    }
236                    FlowBlockType::Sep => {
237                        sdf.box(
238                            0.,
239                            1.,
240                            self.rect_size.x-1,
241                            self.rect_size.y-2.,
242                            2.
243                        );
244                        sdf.fill(self.sep_color);
245                        return sdf.result;
246                    }
247                    FlowBlockType::Code => {
248                        sdf.box(
249                            0.,
250                            0.,
251                            self.rect_size.x,
252                            self.rect_size.y,
253                            2.
254                        );
255                        sdf.fill(self.code_color);
256                        return sdf.result;
257                    }
258                    FlowBlockType::InlineCode => {
259                        sdf.box(
260                            1.,
261                            1.,
262                            self.rect_size.x,
263                            self.rect_size.y - 2.,
264                            2.
265                        );
266                        sdf.fill(self.code_color);
267                        return sdf.result;
268                    }
269                    FlowBlockType::Underline => {
270                        sdf.box(
271                            0.,
272                            self.rect_size.y-2,
273                            self.rect_size.x,
274                            2.0,
275                            0.5
276                        );
277                        sdf.fill(self.line_color);
278                        return sdf.result;
279                    }
280                    FlowBlockType::Strikethrough => {
281                        sdf.box(
282                            0.,
283                            self.rect_size.y * 0.45,
284                            self.rect_size.x,
285                            2.0,
286                            0.5
287                        );
288                        sdf.fill(self.line_color);
289                        return sdf.result;
290                    }
291                }
292                return #f00
293            }
294        }
295        
296        link = <MarkdownLink> {}
297    }
298    
299} 
300
301#[derive(Live, LiveHook, Widget)]
302pub struct Markdown{
303    #[deref] text_flow: TextFlow,
304    #[live] body: ArcStringMut,
305    #[live] paragraph_spacing: f64,
306    #[live] pre_code_spacing: f64,
307    #[live(false)] use_code_block_widget:bool,
308    #[rust] in_code_block: bool,
309    #[rust] code_block_string: String,
310    #[rust] auto_id: u64
311}
312
313impl Widget for Markdown {
314    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
315        self.text_flow.handle_event(cx, event, scope);
316    } 
317    
318    fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk:Walk)->DrawStep{
319        self.auto_id = 0;
320        self.begin(cx, walk);
321        self.process_markdown_doc(cx);
322        self.end(cx);
323        DrawStep::done()
324    }
325     
326    fn text(&self)->String{
327        self.body.as_ref().to_string()
328    } 
329    
330    fn set_text(&mut self, cx:&mut Cx, v:&str){
331        if self.body.as_ref() != v{
332            self.body.set(v);
333            self.redraw(cx);
334        }
335    }
336}
337
338impl Markdown {
339    fn process_markdown_doc(&mut self, cx: &mut Cx2d) {
340        let tf = &mut self.text_flow;
341        // Track state for nested formatting
342        let mut list_stack = Vec::new();
343
344        let parser = Parser::new_ext(self.body.as_ref(), Options::ENABLE_TABLES);        
345        
346        for event in parser.into_iter() {
347            match event {
348                MdEvent::Start(Tag::Heading { level, .. }) => {
349                    cx.turtle_new_line_with_spacing(self.paragraph_spacing);
350                    let levelf64 = match level {
351                        HeadingLevel::H1 => 1.0,
352                        HeadingLevel::H2 => 2.0,
353                        HeadingLevel::H3 => 3.0,
354                        HeadingLevel::H4 => 4.0,
355                        HeadingLevel::H5 => 5.0,
356                        HeadingLevel::H6 => 6.0,
357                    };
358                    tf.push_size_abs_scale(4.5 / levelf64);
359                    tf.bold.push();
360                }
361                MdEvent::End(TagEnd::Heading(_level)) => {
362                    tf.bold.pop();
363                    tf.font_sizes.pop();
364                    cx.turtle_new_line();
365                }
366                MdEvent::Start(Tag::Paragraph) => {
367                    cx.turtle_new_line_with_spacing(self.paragraph_spacing);
368                }
369                MdEvent::End(TagEnd::Paragraph) => {
370                    // No special handling needed
371                }
372                MdEvent::Start(Tag::BlockQuote(_)) => {
373                    cx.turtle_new_line_with_spacing(self.paragraph_spacing);
374                    tf.begin_quote(cx);
375                }
376                MdEvent::End(TagEnd::BlockQuote(_quote_kind)) => {
377                    tf.end_quote(cx);
378                }
379                MdEvent::Start(Tag::List(first_number)) => {
380                    list_stack.push(first_number);
381                }
382                MdEvent::End(TagEnd::List(_is_ordered)) => {
383                    list_stack.pop();
384                }
385                MdEvent::Start(Tag::Item) => {
386                    cx.turtle_new_line();
387                    let marker = if let Some(Some(n)) = list_stack.last() {
388                        format!("{}.", n)
389                    } else {
390                        "•".to_string()
391                    };
392                    tf.begin_list_item(cx, &marker, 1.5);
393                }
394                MdEvent::End(TagEnd::Item) => {
395                    tf.end_list_item(cx);
396                }
397                MdEvent::Start(Tag::Emphasis) => {
398                    tf.italic.push();
399                }
400                MdEvent::End(TagEnd::Emphasis) => {
401                    tf.italic.pop();
402                }
403                MdEvent::Start(Tag::Strong) => {
404                    tf.bold.push();
405                }
406                MdEvent::End(TagEnd::Strong) => {
407                    tf.bold.pop();
408                }
409                MdEvent::Start(Tag::Strikethrough) => {
410                    tf.underline.push();
411                }
412                MdEvent::End(TagEnd::Strikethrough) => {
413                    tf.underline.pop();
414                }
415                MdEvent::Start(Tag::Link { dest_url, .. }) => {
416                    self.auto_id += 1;
417                    let item = tf.item(cx, LiveId(self.auto_id), live_id!(link));
418                    item.as_markdown_link().set_href(&dest_url);
419                    item.draw_all_unscoped(cx);
420                }
421                MdEvent::End(TagEnd::Link) => {
422                    // Link handling is done in Start event
423                }
424                MdEvent::Start(Tag::Image { dest_url, title, .. }) => {
425                    tf.draw_text(cx, "Image[name:");
426                    tf.draw_text(cx, &title);
427                    tf.draw_text(cx, ", url:");
428                    tf.draw_text(cx, &dest_url);
429                    tf.draw_text(cx, "]");
430                }
431                MdEvent::Start(Tag::CodeBlock(_kind)) => {
432                    if self.use_code_block_widget {
433                        self.in_code_block = true;
434                        self.code_block_string.clear();
435                        cx.turtle_new_line_with_spacing(self.pre_code_spacing);
436                        
437                        // TODO: Handle language info if available for syntax highlighting
438                        // if let CodeBlockKind::Fenced(lang) = kind {
439                        // }
440                    } else {
441                        const FIXED_FONT_SIZE_SCALE: f64 = 0.85;
442                        tf.push_size_rel_scale(FIXED_FONT_SIZE_SCALE);
443                        // alright lets check if we need to use a widget
444                        cx.turtle_new_line_with_spacing(self.paragraph_spacing);
445                        tf.combine_spaces.push(false);
446                        tf.fixed.push();
447                                
448                        // This adjustment is necesary to do not add too much spacing
449                        // between lines inside the code block.
450                        // tf.top_drop.push(0.2);
451                                
452                        tf.begin_code(cx);
453                    }
454                }
455                MdEvent::End(TagEnd::CodeBlock) => {
456                    if self.in_code_block {
457                        self.in_code_block = false;
458                        let entry_id = tf.new_counted_id();
459                        let cbs = &self.code_block_string;
460                        
461                        tf.item_with(cx, entry_id, live_id!(code_block), |cx, item, _tf|{
462                            item.widget(id!(code_view)).set_text(cx, cbs);
463                            item.draw_all_unscoped(cx);
464                        });
465                    }
466                    else{
467                        tf.font_sizes.pop();
468                        //tf.top_drop.pop();
469                        tf.fixed.pop();
470                        tf.combine_spaces.pop();
471                        tf.end_code(cx);
472                    }
473                }
474                // Inline code
475                MdEvent::Code(text) => {
476                    const FIXED_FONT_SIZE_SCALE: f64 = 0.85;
477                    tf.push_size_rel_scale(FIXED_FONT_SIZE_SCALE);
478                    tf.fixed.push();
479                    tf.inline_code.push();
480                    tf.draw_text(cx, &text);
481                    tf.font_sizes.pop();
482                    tf.fixed.pop();
483                    tf.inline_code.pop();
484                }
485                MdEvent::Text(text) => {
486                    if self.in_code_block {
487                        self.code_block_string.push_str(&text);
488                    } else {
489                        tf.draw_text(cx, &text.trim_end_matches("\n"));
490                    }
491                }
492                MdEvent::SoftBreak => {
493                    if self.in_code_block {
494                        self.code_block_string.push('\n');
495                    } else {
496                        cx.turtle_new_line();
497                    }
498                }
499                MdEvent::HardBreak => {
500                    if self.in_code_block {
501                        self.code_block_string.push('\n');
502                    } else {
503                        cx.turtle_new_line_with_spacing(self.paragraph_spacing);
504                    }
505                }
506                MdEvent::Rule => {
507                    cx.turtle_new_line_with_spacing(self.paragraph_spacing);
508                    tf.sep(cx);
509                }
510                MdEvent::TaskListMarker(_) => {
511                    // TODO: Implement task list markers
512                }
513                MdEvent::Start(Tag::Table(_)) => {
514                    // TODO: Implement table support
515                }
516                MdEvent::End(TagEnd::Table) => {
517                    // TODO: Implement table support
518                }
519                MdEvent::Start(Tag::TableHead) => {
520                    // TODO: Implement table header support
521                }
522                MdEvent::End(TagEnd::TableHead) => {
523                    // TODO: Implement table header support
524                }
525                MdEvent::Start(Tag::TableRow) => {
526                    // TODO: Implement table row support
527                }
528                MdEvent::End(TagEnd::TableRow) => {
529                    // TODO: Implement table row support
530                }
531                MdEvent::Start(Tag::TableCell) => {
532                    // TODO: Implement table cell support
533                }
534                MdEvent::End(TagEnd::TableCell) => {
535                    // TODO: Implement table cell support
536                }
537                _ => {} // Unimplemented or unneceary events
538            }
539        }
540    }
541}
542
543impl MarkdownRef {
544    pub fn set_text(&mut self, cx:&mut Cx, v:&str) {
545        let Some(mut inner) = self.borrow_mut() else { return };
546        inner.set_text(cx, v)
547    }
548}
549
550#[derive(Live, LiveHook, Widget)]
551struct MarkdownLink {
552    #[deref]
553    link: LinkLabel,
554    #[live]
555    href: String,
556}
557
558impl WidgetMatchEvent for MarkdownLink {
559    fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, scope: &mut Scope) {
560        if self.link.clicked(actions) {
561            cx.widget_action(
562                self.widget_uid(),
563                &scope.path,
564                MarkdownAction::LinkNavigated(self.href.clone()),
565            );
566        }
567    }
568}
569
570impl Widget for MarkdownLink {
571    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
572        self.link.handle_event(cx, event, scope);
573        self.widget_match_event(cx, event, scope)
574    }
575
576    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
577        self.link.draw_walk(cx, scope, walk)
578    }
579
580    fn text(&self) -> String {
581        self.link.text()
582    }
583
584    fn set_text(&mut self, cx:&mut Cx, v: &str) {
585        self.link.set_text(cx, v);
586    }
587}
588
589impl MarkdownLinkRef {
590    pub fn set_href(&self, v: &str) {
591        let Some(mut inner) = self.borrow_mut() else {
592            return;
593        };
594        inner.href = v.to_string();
595    }
596}
597
598#[derive(Clone, Debug, DefaultNone)]
599pub enum MarkdownAction {
600    None,
601    LinkNavigated(String),
602}
603