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 }
26
27 pub MarkdownBase = {{Markdown}} {
28 }
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 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 }
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 }
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 } else {
441 const FIXED_FONT_SIZE_SCALE: f64 = 0.85;
442 tf.push_size_rel_scale(FIXED_FONT_SIZE_SCALE);
443 cx.turtle_new_line_with_spacing(self.paragraph_spacing);
445 tf.combine_spaces.push(false);
446 tf.fixed.push();
447
448 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.fixed.pop();
470 tf.combine_spaces.pop();
471 tf.end_code(cx);
472 }
473 }
474 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 }
513 MdEvent::Start(Tag::Table(_)) => {
514 }
516 MdEvent::End(TagEnd::Table) => {
517 }
519 MdEvent::Start(Tag::TableHead) => {
520 }
522 MdEvent::End(TagEnd::TableHead) => {
523 }
525 MdEvent::Start(Tag::TableRow) => {
526 }
528 MdEvent::End(TagEnd::TableRow) => {
529 }
531 MdEvent::Start(Tag::TableCell) => {
532 }
534 MdEvent::End(TagEnd::TableCell) => {
535 }
537 _ => {} }
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