makepad_widgets/
html.rs

1use crate::{
2    makepad_derive_widget::*,
3    makepad_draw::*,
4    makepad_html::*,
5    text_flow::TextFlow,
6    widget::*,
7};
8
9const BULLET: &str = "•";
10
11live_design!{
12    link widgets;
13    use link::theme::*;
14    use makepad_draw::shader::std::*;
15     
16    pub HtmlLinkBase = {{HtmlLink}} {
17        /*link = {
18            draw_text:{
19                // other blue hyperlink colors: #1a0dab, // #0969da  // #0c50d1
20                color: #1a0dab
21            }
22        }*/
23    }
24
25    pub HtmlBase = {{Html}} {
26        // ok so we can use one drawtext
27        // change to italic, change bold (SDF), strikethrough
28        ul_markers: ["•", "-"],
29        ol_markers: [Numbers, LowerAlpha, LowerRoman],
30        ol_separator: ".",
31    }
32    
33    pub HtmlLink = <HtmlLinkBase> {
34        width: Fit, height: Fit,
35        align: {x: 0., y: 0.}
36        
37        color: #x0000EE,
38        hover_color: #x00EE00,
39        pressed_color: #xEE0000,
40                
41        // instance hovered: 0.0
42        // instance pressed: 0.0
43        
44        animator: {
45            hover = {
46                default: off,
47                off = {
48                    redraw: true,
49                    from: {all: Forward {duration: 0.01}}
50                    apply: {
51                        hovered: 0.0,
52                        pressed: 0.0,
53                    }
54                }
55                
56                on = {
57                    redraw: true,
58                    from: {
59                        all: Forward {duration: 0.1}
60                        pressed: Forward {duration: 0.01}
61                    }
62                    apply: {
63                        hovered: [{time: 0.0, value: 1.0}],
64                        pressed: [{time: 0.0, value: 1.0}],
65                    }
66                }
67                
68                pressed = {
69                    redraw: true,
70                    from: {all: Forward {duration: 0.01}}
71                    apply: {
72                        hovered: [{time: 0.0, value: 1.0}],
73                        pressed: [{time: 0.0, value: 1.0}],
74                    }
75                }
76            }
77        }
78    }
79    
80    pub Html = <HtmlBase> {
81        width: Fill, height: Fit,
82        flow: RightWrap,
83        width:Fill,
84        height:Fit,
85        padding: <THEME_MSPACE_1> {}
86        heading_margin: {top:1.0, bottom:0.1}
87        paragraph_margin: {top: 0.33, bottom:0.33}
88        font_size: (THEME_FONT_SIZE_P),
89        font_color: (THEME_COLOR_LABEL_OUTER),
90        
91        draw_normal: {
92            text_style: <THEME_FONT_REGULAR> {
93                font_size: (THEME_FONT_SIZE_P)
94            }
95            color: (THEME_COLOR_LABEL_OUTER)
96        }
97        
98        draw_italic: {
99            text_style: <THEME_FONT_ITALIC> {
100                font_size: (THEME_FONT_SIZE_P)
101            }
102            color: (THEME_COLOR_LABEL_OUTER)
103        }
104        
105        draw_bold: {
106            text_style: <THEME_FONT_BOLD> {
107                font_size: (THEME_FONT_SIZE_P)
108            }
109            color: (THEME_COLOR_LABEL_OUTER)
110        }
111        
112        draw_bold_italic: {
113            text_style: <THEME_FONT_BOLD_ITALIC> {
114                font_size: (THEME_FONT_SIZE_P)
115            }
116            color: (THEME_COLOR_LABEL_OUTER)
117        }
118        
119        draw_fixed: {
120            temp_y_shift: 0.24
121            text_style: <THEME_FONT_CODE> {
122                font_size: (THEME_FONT_SIZE_P)
123            }
124            color: (THEME_COLOR_LABEL_OUTER)
125        }
126        
127        code_layout: {
128            flow: RightWrap,
129            padding: <THEME_MSPACE_2> {left: (THEME_SPACE_3), right: (THEME_SPACE_3) }
130        }
131        code_walk: { width: Fill, height: Fit }
132        
133        quote_layout: {
134            flow: RightWrap,
135            padding: <THEME_MSPACE_2> { left: (THEME_SPACE_3), right: (THEME_SPACE_3) }
136        }
137        quote_walk: { width: Fill, height: Fit, }
138        
139        list_item_layout: {
140            flow: RightWrap,
141            padding: <THEME_MSPACE_1> {}
142        }
143        list_item_walk: {
144            height: Fit, width: Fill,
145        }
146        
147        inline_code_padding: <THEME_MSPACE_1> {},
148        inline_code_margin: <THEME_MSPACE_1> {},
149        
150        sep_walk: {
151            width: Fill, height: 4.
152            margin: <THEME_MSPACE_V_1> {}
153        }
154        
155        a = <HtmlLink> {}
156        
157        draw_block:{
158            line_color: (THEME_COLOR_LABEL_OUTER)
159            sep_color: (THEME_COLOR_SHADOW)
160            quote_bg_color: (THEME_COLOR_BG_HIGHLIGHT)
161            quote_fg_color: (THEME_COLOR_LABEL_OUTER)
162            code_color: (THEME_COLOR_BG_HIGHLIGHT)
163            fn pixel(self) -> vec4 {
164                let sdf = Sdf2d::viewport(self.pos * self.rect_size);
165                match self.block_type {
166                    FlowBlockType::Quote => {
167                        sdf.box(
168                            0.,
169                            0.,
170                            self.rect_size.x,
171                            self.rect_size.y,
172                            2.
173                        );
174                        sdf.fill(self.quote_bg_color)
175                        sdf.box(
176                            THEME_SPACE_1,
177                            THEME_SPACE_1,
178                            THEME_SPACE_1,
179                            self.rect_size.y - THEME_SPACE_2,
180                            1.5
181                        );
182                        sdf.fill(self.quote_fg_color);
183                        return sdf.result;
184                    }
185                    FlowBlockType::Sep => {
186                        sdf.box(
187                            0.,
188                            1.,
189                            self.rect_size.x-1,
190                            self.rect_size.y-2.,
191                            2.
192                        );
193                        sdf.fill(self.sep_color);
194                        return sdf.result;
195                    }
196                    FlowBlockType::Code => {
197                        sdf.box(
198                            0.,
199                            0.,
200                            self.rect_size.x,
201                            self.rect_size.y,
202                            2.
203                        );
204                        sdf.fill(self.code_color);
205                        return sdf.result;
206                    }
207                    FlowBlockType::InlineCode => {
208                        sdf.box(
209                            1.,
210                            1.,
211                            self.rect_size.x-2.,
212                            self.rect_size.y-2.,
213                            2.
214                        );
215                        sdf.fill(self.code_color);
216                        return sdf.result;
217                    }
218                    FlowBlockType::Underline => {
219                        sdf.box(
220                            0.,
221                            self.rect_size.y-2,
222                            self.rect_size.x,
223                            2.0,
224                            0.5
225                        );
226                        sdf.fill(self.line_color);
227                        return sdf.result;
228                    }
229                    FlowBlockType::Strikethrough => {
230                        sdf.box(
231                            0.,
232                            self.rect_size.y * 0.45,
233                            self.rect_size.x,
234                            2.0,
235                            0.5
236                        );
237                        sdf.fill(self.line_color);
238                        return sdf.result;
239                    }
240                }
241                return #f00
242            }
243        }
244    }
245}
246
247/// Whether to trim leading and trailing whitespace in the text body of an HTML tag.
248///
249/// Currently, *all* Unicode whitespace characters are trimmed, not just ASCII whitespace.
250///
251/// The default is to keep all whitespace.
252#[derive(Copy, Clone, PartialEq, Default)]
253pub enum TrimWhitespaceInText {
254    /// Leading and trailing whitespace will be preserved in the text.
255    #[default]
256    Keep,
257    /// Leading and trailing whitespace will be trimmed from the text.
258    Trim,
259}
260
261#[derive(Live, Widget)]
262pub struct Html {
263    #[deref] pub text_flow: TextFlow,
264    #[live] pub body: ArcStringMut,
265    #[rust] pub doc: HtmlDoc,
266    
267    /// Markers used for unordered lists, indexed by the list's nesting level.
268    /// The marker can be an arbitrary string, such as a bullet point or a custom icon.
269    #[live] ul_markers: Vec<String>,
270    /// Markers used for ordered lists, indexed by the list's nesting level.
271    #[live] ol_markers: Vec<OrderedListType>,
272    /// The character used to separate an ordered list's item number from the content.
273    #[live] ol_separator: String,
274
275    /// The stack of list levels encountered so far, used to track nested lists.
276    #[rust] list_stack: Vec<ListLevel>,
277}
278
279// alright lets parse the HTML
280impl LiveHook for Html {
281    fn after_apply_from(&mut self, _cx: &mut Cx, _apply:&mut Apply) {
282        let mut errors = Some(Vec::new());
283        let new_doc = parse_html(self.body.as_ref(), &mut errors, InternLiveId::No);
284        if new_doc != self.doc{
285            self.doc = new_doc;
286            self.text_flow.clear_items();
287        }
288        if errors.as_ref().unwrap().len()>0{
289            log!("HTML parser returned errors {:?}", errors)
290        }
291    }
292}
293
294impl Html {
295    fn handle_open_tag(
296        cx: &mut Cx2d,
297        tf: &mut TextFlow,
298        node: &mut HtmlWalker,
299        list_stack: &mut Vec<ListLevel>,
300        ul_markers: &Vec<String>,
301        ol_markers: &Vec<OrderedListType>,
302        ol_separator: &str,
303    ) -> (Option<LiveId>, TrimWhitespaceInText) {
304
305        let mut trim_whitespace_in_text = TrimWhitespaceInText::default();
306
307        fn open_header_tag(cx: &mut Cx2d, tf: &mut TextFlow, scale: f64, trim: &mut TrimWhitespaceInText) {
308            *trim = TrimWhitespaceInText::Trim;
309            tf.bold.push();
310            tf.push_size_abs_scale(scale);
311            let fs = *tf.font_sizes.last().unwrap_or(&tf.font_size) as f64;
312            tf.new_line_collapsed_with_spacing(cx, fs * tf.heading_margin.top);
313        }
314
315        match node.open_tag_lc() {
316            some_id!(h1) => open_header_tag(cx, tf, 2.0, &mut trim_whitespace_in_text),
317            some_id!(h2) => open_header_tag(cx, tf, 1.5, &mut trim_whitespace_in_text),
318            some_id!(h3) => open_header_tag(cx, tf, 1.17, &mut trim_whitespace_in_text),
319            some_id!(h4) => open_header_tag(cx, tf, 1.0, &mut trim_whitespace_in_text),
320            some_id!(h5) => open_header_tag(cx, tf, 0.83, &mut trim_whitespace_in_text),
321            some_id!(h6) => open_header_tag(cx, tf, 0.67, &mut trim_whitespace_in_text),
322
323            some_id!(p) => {
324                // there's probably a better way to do this by setting margins...
325                let fs = *tf.font_sizes.last().unwrap_or(&tf.font_size) as f64;
326                
327                tf.new_line_collapsed_with_spacing(cx, fs * tf.paragraph_margin.top);
328                //tf.new_line_collapsed(cx);
329                trim_whitespace_in_text = TrimWhitespaceInText::Trim;
330            }
331            some_id!(code) => {
332                const FIXED_FONT_SIZE_SCALE: f64 = 0.85;
333                tf.push_size_rel_scale(FIXED_FONT_SIZE_SCALE);
334                //tf.top_drop.push(1.2/FIXED_FONT_SIZE_SCALE); // to achieve a top_drop of 1.2
335                tf.combine_spaces.push(false);
336                tf.fixed.push();
337                tf.inline_code.push();
338            }
339            some_id!(pre) => {
340                tf.new_line_collapsed(cx);
341                tf.fixed.push();
342                tf.ignore_newlines.push(false);
343                tf.combine_spaces.push(false);
344                tf.begin_code(cx);
345            }
346            some_id!(blockquote) => {
347                tf.new_line_collapsed(cx);
348                tf.ignore_newlines.push(false);
349                tf.combine_spaces.push(false);
350                tf.begin_quote(cx);
351                trim_whitespace_in_text = TrimWhitespaceInText::Trim;
352            }
353            some_id!(br) => {
354                tf.new_line_collapsed(cx);
355                trim_whitespace_in_text = TrimWhitespaceInText::Trim;
356            }
357            some_id!(hr)
358            | some_id!(sep) => {
359                tf.new_line_collapsed(cx);
360                tf.sep(cx);
361                tf.new_line_collapsed(cx);
362                trim_whitespace_in_text = TrimWhitespaceInText::Trim;
363            }
364            some_id!(u) => tf.underline.push(),
365            some_id!(del)
366            | some_id!(s)
367            | some_id!(strike) => tf.strikethrough.push(),
368
369            some_id!(b)
370            | some_id!(strong) => tf.bold.push(),
371            some_id!(i)
372            | some_id!(em) => tf.italic.push(),
373
374            some_id!(sub) => {
375                // Adjust the top drop to move the text slightly downwards.
376                //let curr_top_drop = tf.top_drop.last()
377                 //   .unwrap_or(&1.2);
378                // A 55% increase in top_drop seems to look good for subscripts,
379                // which should be slightly below the halfway point in the line
380                //let new_top_drop = curr_top_drop * 1.55;
381                //tf.top_drop.push(new_top_drop);
382                tf.push_size_rel_scale(0.7);
383            }
384            some_id!(sup) => {
385                tf.push_size_rel_scale(0.7);
386            }
387            some_id!(ul) => {
388                trim_whitespace_in_text = TrimWhitespaceInText::Trim;
389                list_stack.push(ListLevel {
390                    list_kind: ListKind::Unordered,
391                    numbering_type: None,
392                    li_count: 1,
393                    padding: 2.5,
394                });
395            }
396            some_id!(ol) => { 
397                trim_whitespace_in_text = TrimWhitespaceInText::Trim;
398                // Handle the "start" attribute
399                let start_attr = node.find_attr_lc(live_id!(start));
400                let start: i32 = start_attr
401                    .and_then(|s| s.parse().ok())
402                    .unwrap_or(1);
403
404                // Handle the "type" attribute
405                let type_attr = node.find_attr_lc(live_id!(type));
406                let numbering_type = type_attr.and_then(OrderedListType::from_type_attribute);
407
408                list_stack.push(ListLevel {
409                    list_kind: ListKind::Ordered,
410                    numbering_type, 
411                    li_count: start,
412                    padding: 2.5,
413                });
414            }
415            some_id!(li) => {
416                trim_whitespace_in_text = TrimWhitespaceInText::Trim;
417                let indent_level = list_stack.len();
418                let index = indent_level.saturating_sub(1);
419                // log!("indent_level: {indent_level}, index: {index}, list_stack: {list_stack:?}");
420                let marker_and_pad = list_stack.last_mut().map(|ll| {
421                    let marker = match ll.list_kind {
422                        ListKind::Unordered => {
423                            ul_markers.get(index).cloned()
424                                .unwrap_or_else(|| BULLET.into()) // default to bullet point
425                        }
426                        ListKind::Ordered => {
427                            // Handle the "value" attribute, only relevant to <ol>.
428                            let value_attr = node.find_attr_lc(live_id!(value));
429                            let value: i32 = value_attr
430                                .and_then(|s| s.parse().ok())
431                                .unwrap_or(ll.li_count);
432
433                            // Handle the "type" attribute, only relevant to <ol>.
434                            let type_attr = node.find_attr_lc(live_id!(type));
435                            let numbering_type = type_attr.and_then(OrderedListType::from_type_attribute);
436
437                            // Generate this <li> marker string using either:
438                            // * the <li> element's numbering type, otherwise,
439                            // * the outer <ol>'s numbering type, otherwise,
440                            // * the DSL-specified numbering type for the current nesting level,
441                            // * otherwise a literal "#" character, which indicates malformed HTML.
442                            numbering_type.as_ref()
443                                .or_else(|| ll.numbering_type.as_ref())
444                                .or_else(|| ol_markers.get(index))
445                                .map(|ol_type| ol_type.marker(value, ol_separator))
446                                .unwrap_or_else(|| "#".into())
447                        }
448                    };
449                    ll.li_count += 1;
450                    (marker, ll.padding)
451                });
452                let (marker, pad) = marker_and_pad.as_ref()
453                    .map(|(m, p)| (m.as_str(), *p))
454                    .unwrap_or((BULLET, 2.5));
455                
456                // Now, actually emit the list item.
457                // log!("marker: {marker}, pad: {pad}");
458                // ok so what if we only have drawn whitespace here
459                tf.new_line_collapsed(cx);
460                tf.begin_list_item(cx, marker, pad);
461            }
462            Some(x) => return (Some(x), trim_whitespace_in_text),
463            _ => ()
464        }
465        (None, trim_whitespace_in_text)
466    }
467    
468    fn handle_close_tag(
469        cx: &mut Cx2d,
470        tf: &mut TextFlow,
471        node: &mut HtmlWalker,
472        list_stack: &mut Vec<ListLevel>,
473    ) -> Option<LiveId> {
474        match node.close_tag_lc() {
475            some_id!(h1)
476            | some_id!(h2)
477            | some_id!(h3)
478            | some_id!(h4)
479            | some_id!(h5)
480            | some_id!(h6) => {
481                let size = tf.font_sizes.pop();
482                tf.bold.pop();
483                tf.new_line_collapsed_with_spacing(cx, size.unwrap_or(0.0) as f64 * tf.heading_margin.bottom);
484                // we wanna add extra spacing here
485                
486            }
487            some_id!(b)
488            | some_id!(strong) => tf.bold.pop(),
489            some_id!(i)
490            | some_id!(em) => tf.italic.pop(),
491            some_id!(p) => {
492                let fs = *tf.font_sizes.last().unwrap_or(&tf.font_size) as f64;
493                 tf.new_line_collapsed_with_spacing(cx, fs * tf.paragraph_margin.bottom);
494                //tf.new_line_collapsed(cx);
495            }
496            some_id!(blockquote) => {
497                tf.ignore_newlines.pop();
498                tf.combine_spaces.pop();
499                tf.end_quote(cx);
500            }
501            some_id!(code) => {
502                
503                tf.inline_code.pop();
504                //tf.top_drop.pop();
505                tf.font_sizes.pop();
506                tf.combine_spaces.pop();
507                tf.fixed.pop(); 
508            }
509            some_id!(pre) => {
510                tf.fixed.pop();
511                tf.ignore_newlines.pop();
512                tf.combine_spaces.pop();
513                tf.end_code(cx);     
514            }
515            some_id!(sub)=>{
516                //tf.top_drop.pop();
517                tf.font_sizes.pop();
518            }
519            some_id!(sup) => {
520                tf.font_sizes.pop();
521            }
522            some_id!(ul)
523            | some_id!(ol) => {
524                list_stack.pop();
525            }
526            some_id!(li) => tf.end_list_item(cx),
527            some_id!(u) => tf.underline.pop(),
528            some_id!(del)
529            | some_id!(s)
530            | some_id!(strike) => tf.strikethrough.pop(),
531            _ => ()
532        }
533        None
534    }
535    
536    pub fn handle_text_node(
537        cx: &mut Cx2d,
538        tf: &mut TextFlow,
539        node: &mut HtmlWalker,
540        trim: TrimWhitespaceInText,    
541    ) -> bool {
542        if let Some(text) = node.text() {
543            let text = if trim == TrimWhitespaceInText::Trim {
544                text.trim_matches(char::is_whitespace)
545            } else {
546                text
547            };
548            tf.draw_text(cx, text);
549            true
550        }
551        else {
552            false
553        }
554    }
555}
556
557impl Widget for Html {
558    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
559        self.text_flow.handle_event(cx, event, scope);
560    }
561    
562    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
563        let tf = &mut self.text_flow;
564        tf.begin(cx, walk);
565        // alright lets iterate the html doc and draw it
566        let mut node = self.doc.new_walker();
567        let mut auto_id = 0;
568        while !node.done() {
569            let mut trim = TrimWhitespaceInText::default();
570            match Self::handle_open_tag(cx, tf, &mut node, &mut self.list_stack, &self.ul_markers, &self.ol_markers, &self.ol_separator) {
571                (Some(_), _tws) => {
572                    handle_custom_widget(cx, scope, tf, &self.doc, &mut node, &mut auto_id); 
573                }
574                (None, tws) => {
575                    trim = tws;
576                }
577            }
578            match Self::handle_close_tag(cx, tf, &mut node, &mut self.list_stack) {
579                _ => ()
580            }
581            Self::handle_text_node(cx, tf, &mut node, trim);
582            node.walk();
583        }
584        tf.end(cx);
585        DrawStep::done()
586    }  
587     
588    fn text(&self) -> String {
589        self.body.as_ref().to_string()
590    }
591    
592    fn set_text(&mut self, cx:&mut Cx, v:&str){
593        self.body.set(v);
594        let mut errors = Some(Vec::new());
595        self.doc = parse_html(self.body.as_ref(), &mut errors, InternLiveId::No);
596        if errors.as_ref().unwrap().len()>0{
597            log!("HTML parser returned errors {:?}", errors)
598        }
599        self.redraw(cx);
600    }
601} 
602
603
604fn handle_custom_widget(
605    cx: &mut Cx2d,
606    _scope: &mut Scope,
607    tf: &mut TextFlow,
608    doc: &HtmlDoc,
609    node: &mut HtmlWalker,
610    auto_id: &mut u64,
611) {
612    let id = if let Some(id) = node.find_attr_lc(live_id!(id)) {
613        LiveId::from_str(id)
614    } else {
615        *auto_id += 1;
616        LiveId(*auto_id)
617    };
618
619    let template = node.open_tag_nc().unwrap();
620    // lets grab the nodes+index from the walker
621    let mut scope_with_attrs = Scope::with_props_index(doc, node.index);
622    // log!("FOUND CUSTOM WIDGET! template: {template:?}, id: {id:?}, attrs: {attrs:?}");
623
624    if let Some(item) = tf.item_with_scope(cx, &mut scope_with_attrs, id, template) {
625        item.set_text(cx, node.find_text().unwrap_or(""));
626        let mut draw_scope = Scope::with_data(tf);
627        item.draw_all(cx, &mut draw_scope);
628    }
629
630    node.jump_to_close();
631}
632
633
634#[derive(Debug, Clone, DefaultNone)]
635pub enum HtmlLinkAction {
636    Clicked {
637        url: String,
638        key_modifiers: KeyModifiers,
639    },
640    SecondaryClicked {
641        url: String,
642        key_modifiers: KeyModifiers,
643    },
644    None,
645}
646
647#[derive(Live, Widget)]
648pub struct HtmlLink {
649    #[animator] animator: Animator,
650
651    // TODO: this is unusued; just here to invalidly satisfy the area provider.
652    //       I'm not sure how to implement `fn area()` given that it has multiple area rects.
653    #[redraw] #[area] area: Area,
654
655    // TODO: remove these if they're unneeded
656    #[walk] walk: Walk,
657    #[layout] layout: Layout,
658
659    #[rust] drawn_areas: SmallVec<[Area; 2]>,
660    #[live(true)] grab_key_focus: bool,
661
662    #[live] hovered: f32,
663    #[live] pressed: f32,
664
665    /// The default font color for the link when not hovered on or pressed.
666    #[live] color: Option<Vec4>,
667    /// The font color used when the link is hovered on.
668    #[live] hover_color: Option<Vec4>,
669    /// The font color used when the link is pressed.
670    #[live] pressed_color: Option<Vec4>,
671
672    #[live] pub text: ArcStringMut,
673    #[live] pub url: String,
674}
675
676impl LiveHook for HtmlLink {
677    // After an HtmlLink instance has been instantiated ("applied"),
678    // populate its struct fields from the `<a>` tag's attributes.
679    fn after_apply(&mut self, _cx: &mut Cx, apply: &mut Apply, _index: usize, _nodes: &[LiveNode]) {
680        //log!("HtmlLink::after_apply(): apply.from: {:?}, apply.scope exists: {:?}", apply.from, apply.scope.is_some());
681        match apply.from {
682            ApplyFrom::NewFromDoc {..} => {
683                let scope = apply.scope.as_ref().unwrap();
684                let doc = scope.props.get::<HtmlDoc>().unwrap();
685                let mut walker = doc.new_walker_with_index(scope.index + 1);
686                
687                if let Some((lc, attr)) = walker.while_attr_lc() {
688                    match lc {
689                        live_id!(href)=> {
690                            self.url = attr.into()
691                        }
692                        _=>()
693                    }
694                }
695            }
696            _ => ()
697        }
698    }
699}
700
701impl Widget for HtmlLink {
702    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
703        if self.animator_handle_event(cx, event).must_redraw() {
704            // Currently, this conditional will never be true because the `scope`
705            // isn't yet populated with the Html's TextFlow.
706            if let Some(tf) = scope.data.get_mut::<TextFlow>() {
707                tf.redraw(cx);
708            } else {
709                self.drawn_areas.iter().for_each(|area| area.redraw(cx));
710            }
711
712            // This won't work, as this widget owns no views, so redrawing it does nothing.
713            // self.redraw(cx);
714        }
715
716        for area in self.drawn_areas.clone().into_iter() {
717            match event.hits(cx, area) {
718                Hit::FingerDown(fe) => {
719                    if fe.is_primary_hit() {
720                        if self.grab_key_focus {
721                            cx.set_key_focus(self.area());
722                        }
723                        self.animator_play(cx, id!(hover.pressed));
724                    }
725                    // Fire a secondary click action on a right-click *down* event.
726                    else if fe.mouse_button().is_some_and(|mb| mb.is_secondary()) {
727                        cx.widget_action(
728                            self.widget_uid(),
729                            &scope.path,
730                            HtmlLinkAction::SecondaryClicked {
731                                url: self.url.clone(),
732                                key_modifiers: fe.modifiers,
733                            },
734                        );
735                    }
736                }
737                Hit::FingerHoverIn(_) => {
738                    cx.set_cursor(MouseCursor::Hand);
739                    self.animator_play(cx, id!(hover.on));
740                }
741                Hit::FingerHoverOut(_) => {
742                    self.animator_play(cx, id!(hover.off));
743                }
744                Hit::FingerLongPress(_) => {
745                    cx.widget_action(
746                        self.widget_uid(),
747                        &scope.path,
748                        HtmlLinkAction::SecondaryClicked {
749                            url: self.url.clone(),
750                            key_modifiers: Default::default(),
751                        },
752                    );
753                }
754                Hit::FingerUp(fu) => {
755                    if fu.is_over {
756                        cx.set_cursor(MouseCursor::Hand);
757                        self.animator_play(cx, id!(hover.on));
758                    } else {
759                        self.animator_play(cx, id!(hover.off));
760                    }
761
762                    if fu.is_over
763                        && fu.is_primary_hit()
764                        && fu.was_tap()
765                    {
766                        cx.widget_action(
767                            self.widget_uid(),
768                            &scope.path,
769                            HtmlLinkAction::Clicked {
770                                url: self.url.clone(),
771                                key_modifiers: fu.modifiers,
772                            },
773                        );
774                    }
775                }
776                _ => (),
777            }
778        }
779    }
780    
781    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, _walk: Walk) -> DrawStep {
782        let Some(tf) = scope.data.get_mut::<TextFlow>() else {
783            return DrawStep::done();
784        };
785
786        // Here: the text flow has already began drawing, so we just need to draw the text.
787        tf.underline.push();
788        tf.areas_tracker.push_tracker();
789        let mut pushed_color = false;
790        if self.hovered > 0.0 {
791            if let Some(color) = self.hover_color {
792                tf.font_colors.push(color);
793                pushed_color = true;
794            }
795        } else if self.pressed > 0.0 {
796            if let Some(color) = self.pressed_color {
797                tf.font_colors.push(color);
798                pushed_color = true;
799            }
800        } else {
801            if let Some(color) = self.color {
802                tf.font_colors.push(color);
803                pushed_color = true;
804            }
805        }
806        tf.draw_text(cx, self.text.as_ref());
807        
808        if pushed_color {
809            tf.font_colors.pop();
810        }
811        tf.underline.pop();
812
813        let (start, end) = tf.areas_tracker.pop_tracker();
814        
815        if self.drawn_areas.len() == end-start{
816            for i in 0..end-start{
817                self.drawn_areas[i] = cx.update_area_refs( self.drawn_areas[i], 
818                tf.areas_tracker.areas[i+start]);
819            }
820        }
821        else{
822            self.drawn_areas = SmallVec::from(
823                &tf.areas_tracker.areas[start..end]
824            );
825        }
826
827        DrawStep::done()
828    }
829    
830    fn text(&self) -> String {
831        self.text.as_ref().to_string()
832    }
833
834    fn set_text(&mut self, cx:&mut Cx, v: &str) {
835        self.text.as_mut_empty().push_str(v);
836        self.redraw(cx);
837    }
838}
839
840impl HtmlLinkRef {
841    pub fn set_url(&mut self, url: &str) {
842        if let Some(mut inner) = self.borrow_mut() {
843            inner.url = url.to_string();
844        }
845    }
846
847    pub fn url(&self) -> Option<String> {
848        if let Some(inner) = self.borrow() {
849            Some(inner.url.clone())
850        } else {
851            None
852        }
853    }
854}
855
856/// The format and metadata of a list at a given nesting level.
857#[derive(Debug)]
858struct ListLevel {
859    /// The kind of list, either ordered or unordered.
860    list_kind: ListKind,
861    /// The type of marker formatting for ordered lists,
862    /// if overridden for this particular list level.
863    numbering_type: Option<OrderedListType>,
864    /// The number of list items encountered so far at this level of nesting.
865    /// This is a 1-indexed value, so the default initial value should be 1.
866    /// This is an integer because negative numbering values are supported.
867    li_count: i32,
868    /// The padding space inserted to the left of each list item,
869    /// where the list marker is drawn.
870    padding: f64,
871}
872
873/// List kinds: ordered (numbered) and unordered (bulleted).
874#[derive(Debug)]
875enum ListKind {
876    Unordered,
877    Ordered,
878}
879
880/// The type of marker used for ordered lists.
881///
882/// See the ["type" attribute docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol#attributes)
883/// for more info.
884#[derive(Copy, Clone, Debug, Live, LiveHook)]
885#[live_ignore]
886pub enum OrderedListType {
887    #[pick] // must be the top-most attribute
888    /// Decimal integers: 1, 2, 3, 4, ...
889    ///
890    /// This *does* support negative integer values, e.g., -2, -1, 0, 1, 2 ...
891    Numbers,
892    /// Uppercase letters: A, B, C, D, ...
893    UpperAlpha,
894    /// Lowercase letters: a, b, c, d, ...
895    LowerAlpha,
896    /// Uppercase roman numerals: I, II, III, IV, ...
897    UpperRoman,
898    /// Lowercase roman numerals: i, ii, iii, iv, ...
899    LowerRoman,
900}
901impl Default for OrderedListType {
902    fn default() -> Self {
903        OrderedListType::Numbers
904    }
905}
906impl OrderedListType {
907    /// Returns the marker for the given count and separator character.
908    ///
909    /// ## Notes on behavior
910    /// * A negative or zero `count` will always return an integer number marker.
911    /// * Currently, for `UpperApha` and `LowerAlpha`, a `count` higher than 25 will result in a wrong character.
912    /// * Roman numerals >= 4000 will return an integer number marker.
913    pub fn marker(&self, count: i32, separator: &str) -> String {
914        let to_number = || format!("{count}{separator}");
915        if count <= 0 { return to_number(); }
916
917        match self {
918            OrderedListType::Numbers => to_number(),
919            // TODO: fix alpha implementations
920            OrderedListType::UpperAlpha => format!("{}{separator}", ('A' as u8 + count as u8 - 1) as char),
921            OrderedListType::LowerAlpha => format!("{}{separator}", ('a' as u8 + count as u8 - 1) as char),
922            OrderedListType::UpperRoman => to_roman_numeral(count)
923                .map(|m| format!("{}{separator}", m))
924                .unwrap_or_else(to_number),
925            OrderedListType::LowerRoman => to_roman_numeral(count)
926                .map(|m| format!("{}{separator}", m.to_lowercase()))
927                .unwrap_or_else(to_number),
928        }
929    }
930
931    /// Returns an ordered list type based on the given HTML `type` attribute value string `s`.
932    ///
933    /// Returns `None` if an invalid value is given.
934    pub fn from_type_attribute(s: &str) -> Option<Self> {
935        match s {
936            "a" => Some(OrderedListType::LowerAlpha),
937            "A" => Some(OrderedListType::UpperAlpha),
938            "i" => Some(OrderedListType::LowerRoman),
939            "I" => Some(OrderedListType::UpperRoman),
940            "1" => Some(OrderedListType::Numbers),
941            _ => None,
942        }
943    }
944}
945
946/// Converts an integer into an uppercase roman numeral string.
947///
948/// Returns `None` if the input is not between 1 and 3999 inclusive.
949///
950/// This code was adapted from the [`roman` crate](https://crates.io/crates/roman).
951pub fn to_roman_numeral(mut count: i32) -> Option<String> {
952    const MAX: i32 = 3999;
953    static NUMERALS: &[(i32, &str)] = &[
954        (1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
955        (100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
956        (10, "X"), (9, "IX"), (5, "V"), (4, "IV"),
957        (1, "I")
958    ];
959
960    if count <= 0 || count > MAX { return None; }
961    let mut output = String::new();
962    for &(value, s) in NUMERALS.iter() {
963        while count >= value {
964            count -= value;
965            output.push_str(s);
966        }
967    }
968    if count == 0 {
969        Some(output)
970    } else {
971        None
972    }
973}