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 }
24
25 pub HtmlBase = {{Html}} {
26 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 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#[derive(Copy, Clone, PartialEq, Default)]
253pub enum TrimWhitespaceInText {
254 #[default]
256 Keep,
257 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 #[live] ul_markers: Vec<String>,
270 #[live] ol_markers: Vec<OrderedListType>,
272 #[live] ol_separator: String,
274
275 #[rust] list_stack: Vec<ListLevel>,
277}
278
279impl 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 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 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.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 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 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 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 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()) }
426 ListKind::Ordered => {
427 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 let type_attr = node.find_attr_lc(live_id!(type));
435 let numbering_type = type_attr.and_then(OrderedListType::from_type_attribute);
436
437 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 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 }
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 }
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.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.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 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 let mut scope_with_attrs = Scope::with_props_index(doc, node.index);
622 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 #[redraw] #[area] area: Area,
654
655 #[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 #[live] color: Option<Vec4>,
667 #[live] hover_color: Option<Vec4>,
669 #[live] pressed_color: Option<Vec4>,
671
672 #[live] pub text: ArcStringMut,
673 #[live] pub url: String,
674}
675
676impl LiveHook for HtmlLink {
677 fn after_apply(&mut self, _cx: &mut Cx, apply: &mut Apply, _index: usize, _nodes: &[LiveNode]) {
680 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 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 }
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 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 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#[derive(Debug)]
858struct ListLevel {
859 list_kind: ListKind,
861 numbering_type: Option<OrderedListType>,
864 li_count: i32,
868 padding: f64,
871}
872
873#[derive(Debug)]
875enum ListKind {
876 Unordered,
877 Ordered,
878}
879
880#[derive(Copy, Clone, Debug, Live, LiveHook)]
885#[live_ignore]
886pub enum OrderedListType {
887 #[pick] Numbers,
892 UpperAlpha,
894 LowerAlpha,
896 UpperRoman,
898 LowerRoman,
900}
901impl Default for OrderedListType {
902 fn default() -> Self {
903 OrderedListType::Numbers
904 }
905}
906impl OrderedListType {
907 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 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 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
946pub 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}