use std::num::NonZeroU32;
pub use pulldown_cmark::HeadingLevel;
use zng_ext_font::*;
use zng_ext_image::ImageSource;
use zng_ext_input::gesture::ClickArgs;
use zng_wgt::*;
use zng_wgt_access::{self as access, access_role, AccessRole};
use zng_wgt_button::{Button, LinkStyle};
use zng_wgt_container::{child_align, padding, Container};
use zng_wgt_fill::background_color;
use zng_wgt_filter::opacity;
use zng_wgt_grid::{self as grid, Grid};
use zng_wgt_size_offset::{offset, size};
use zng_wgt_stack::{Stack, StackDirection};
use zng_wgt_text::{font_size, font_weight, Text, FONT_COLOR_VAR, PARAGRAPH_SPACING_VAR};
use zng_wgt_tooltip::*;
use zng_wgt_transform::scale;
use zng_wgt_wrap::Wrap;
use super::*;
#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct MarkdownStyle {
pub strong: bool,
pub emphasis: bool,
pub strikethrough: bool,
}
pub struct TextFnArgs {
pub txt: Txt,
pub style: MarkdownStyle,
}
pub struct LinkFnArgs {
pub url: Txt,
pub title: Txt,
pub items: UiVec,
}
pub struct CodeInlineFnArgs {
pub txt: Txt,
pub style: MarkdownStyle,
}
pub struct CodeBlockFnArgs {
pub lang: Txt,
pub txt: Txt,
}
pub struct ParagraphFnArgs {
pub index: u32,
pub items: UiVec,
}
pub struct HeadingFnArgs {
pub level: HeadingLevel,
pub anchor: Txt,
pub items: UiVec,
}
pub struct ListFnArgs {
pub depth: u32,
pub first_num: Option<u64>,
pub items: UiVec,
}
#[derive(Clone, Copy)]
pub struct ListItemBulletFnArgs {
pub depth: u32,
pub num: Option<u64>,
pub checked: Option<bool>,
}
pub struct ListItemFnArgs {
pub bullet: ListItemBulletFnArgs,
pub items: UiVec,
pub blocks: UiVec,
}
pub struct DefListArgs {
pub items: UiVec,
}
pub struct DefListItemTitleArgs {
pub items: UiVec,
}
pub struct DefListItemDefinitionArgs {
pub items: UiVec,
}
pub struct ImageFnArgs {
pub source: ImageSource,
pub title: Txt,
pub alt_items: UiVec,
pub alt_txt: Txt,
}
pub struct RuleFnArgs {}
pub struct BlockQuoteFnArgs {
pub level: u32,
pub items: UiVec,
}
pub struct FootnoteRefFnArgs {
pub label: Txt,
}
pub struct FootnoteDefFnArgs {
pub label: Txt,
pub items: UiVec,
}
pub struct TableFnArgs {
pub columns: Vec<Align>,
pub cells: UiVec,
}
pub struct TableCellFnArgs {
pub is_heading: bool,
pub col_align: Align,
pub items: UiVec,
}
pub struct PanelFnArgs {
pub items: UiVec,
}
context_var! {
pub static TEXT_FN_VAR: WidgetFn<TextFnArgs> = WidgetFn::new(default_text_fn);
pub static LINK_FN_VAR: WidgetFn<LinkFnArgs> = WidgetFn::new(default_link_fn);
pub static CODE_INLINE_FN_VAR: WidgetFn<CodeInlineFnArgs> = WidgetFn::new(default_code_inline_fn);
pub static CODE_BLOCK_FN_VAR: WidgetFn<CodeBlockFnArgs> = WidgetFn::new(default_code_block_fn);
pub static PARAGRAPH_FN_VAR: WidgetFn<ParagraphFnArgs> = WidgetFn::new(default_paragraph_fn);
pub static HEADING_FN_VAR: WidgetFn<HeadingFnArgs> = WidgetFn::new(default_heading_fn);
pub static LIST_FN_VAR: WidgetFn<ListFnArgs> = WidgetFn::new(default_list_fn);
pub static LIST_ITEM_BULLET_FN_VAR: WidgetFn<ListItemBulletFnArgs> = WidgetFn::new(default_list_item_bullet_fn);
pub static LIST_ITEM_FN_VAR: WidgetFn<ListItemFnArgs> = WidgetFn::new(default_list_item_fn);
pub static DEF_LIST_FN_VAR: WidgetFn<DefListArgs> = WidgetFn::new(default_def_list_fn);
pub static DEF_LIST_ITEM_TITLE_FN_VAR: WidgetFn<DefListItemTitleArgs> = WidgetFn::new(default_def_list_item_title_fn);
pub static DEF_LIST_ITEM_DEFINITION_FN_VAR: WidgetFn<DefListItemDefinitionArgs> = WidgetFn::new(default_def_list_item_definition_fn);
pub static IMAGE_FN_VAR: WidgetFn<ImageFnArgs> = WidgetFn::new(default_image_fn);
pub static RULE_FN_VAR: WidgetFn<RuleFnArgs> = WidgetFn::new(default_rule_fn);
pub static BLOCK_QUOTE_FN_VAR: WidgetFn<BlockQuoteFnArgs> = WidgetFn::new(default_block_quote_fn);
pub static FOOTNOTE_REF_FN_VAR: WidgetFn<FootnoteRefFnArgs> = WidgetFn::new(default_footnote_ref_fn);
pub static FOOTNOTE_DEF_FN_VAR: WidgetFn<FootnoteDefFnArgs> = WidgetFn::new(default_footnote_def_fn);
pub static TABLE_FN_VAR: WidgetFn<TableFnArgs> = WidgetFn::new(default_table_fn);
pub static TABLE_CELL_FN_VAR: WidgetFn<TableCellFnArgs> = WidgetFn::new(default_table_cell_fn);
pub static PANEL_FN_VAR: WidgetFn<PanelFnArgs> = WidgetFn::new(default_panel_fn);
}
#[property(CONTEXT, default(TEXT_FN_VAR), widget_impl(Markdown))]
pub fn text_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<TextFnArgs>>) -> impl UiNode {
with_context_var(child, TEXT_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(LINK_FN_VAR), widget_impl(Markdown))]
pub fn link_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<LinkFnArgs>>) -> impl UiNode {
with_context_var(child, LINK_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(CODE_INLINE_FN_VAR), widget_impl(Markdown))]
pub fn code_inline_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<CodeInlineFnArgs>>) -> impl UiNode {
with_context_var(child, CODE_INLINE_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(CODE_BLOCK_FN_VAR), widget_impl(Markdown))]
pub fn code_block_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<CodeBlockFnArgs>>) -> impl UiNode {
with_context_var(child, CODE_BLOCK_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(PARAGRAPH_FN_VAR), widget_impl(Markdown))]
pub fn paragraph_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<ParagraphFnArgs>>) -> impl UiNode {
with_context_var(child, PARAGRAPH_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(HEADING_FN_VAR), widget_impl(Markdown))]
pub fn heading_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<HeadingFnArgs>>) -> impl UiNode {
with_context_var(child, HEADING_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(LIST_FN_VAR), widget_impl(Markdown))]
pub fn list_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<ListFnArgs>>) -> impl UiNode {
with_context_var(child, LIST_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(DEF_LIST_FN_VAR), widget_impl(Markdown))]
pub fn def_list_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<DefListArgs>>) -> impl UiNode {
with_context_var(child, DEF_LIST_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(DEF_LIST_ITEM_TITLE_FN_VAR), widget_impl(Markdown))]
pub fn def_list_item_title_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<DefListItemTitleArgs>>) -> impl UiNode {
with_context_var(child, DEF_LIST_ITEM_TITLE_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(DEF_LIST_ITEM_DEFINITION_FN_VAR), widget_impl(Markdown))]
pub fn def_list_item_definition_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<DefListItemDefinitionArgs>>) -> impl UiNode {
with_context_var(child, DEF_LIST_ITEM_DEFINITION_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(LIST_ITEM_BULLET_FN_VAR), widget_impl(Markdown))]
pub fn list_item_bullet_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<ListItemBulletFnArgs>>) -> impl UiNode {
with_context_var(child, LIST_ITEM_BULLET_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(LIST_ITEM_FN_VAR), widget_impl(Markdown))]
pub fn list_item_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<ListItemFnArgs>>) -> impl UiNode {
with_context_var(child, LIST_ITEM_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(IMAGE_FN_VAR), widget_impl(Markdown))]
pub fn image_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<ImageFnArgs>>) -> impl UiNode {
with_context_var(child, IMAGE_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(RULE_FN_VAR), widget_impl(Markdown))]
pub fn rule_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<RuleFnArgs>>) -> impl UiNode {
with_context_var(child, RULE_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(BLOCK_QUOTE_FN_VAR), widget_impl(Markdown))]
pub fn block_quote_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<BlockQuoteFnArgs>>) -> impl UiNode {
with_context_var(child, BLOCK_QUOTE_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(FOOTNOTE_REF_FN_VAR), widget_impl(Markdown))]
pub fn footnote_ref_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<FootnoteRefFnArgs>>) -> impl UiNode {
with_context_var(child, FOOTNOTE_REF_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(FOOTNOTE_DEF_FN_VAR), widget_impl(Markdown))]
pub fn footnote_def_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<FootnoteDefFnArgs>>) -> impl UiNode {
with_context_var(child, FOOTNOTE_DEF_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(TABLE_FN_VAR), widget_impl(Markdown))]
pub fn table_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<TableFnArgs>>) -> impl UiNode {
with_context_var(child, TABLE_FN_VAR, wgt_fn)
}
#[property(CONTEXT, default(PANEL_FN_VAR), widget_impl(Markdown))]
pub fn panel_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<PanelFnArgs>>) -> impl UiNode {
with_context_var(child, PANEL_FN_VAR, wgt_fn)
}
fn text_view_builder(txt: Txt, style: MarkdownStyle) -> Text {
let mut builder = Text::widget_new();
widget_set! {
&mut builder;
txt;
}
if style.strong {
widget_set! {
&mut builder;
font_weight = FontWeight::BOLD;
}
}
if style.emphasis {
widget_set! {
&mut builder;
font_style = FontStyle::Italic;
}
}
if style.strikethrough {
widget_set! {
&mut builder;
strikethrough = 1, LineStyle::Solid;
}
}
builder
}
pub fn default_text_fn(args: TextFnArgs) -> impl UiNode {
let mut builder = text_view_builder(args.txt, args.style);
builder.widget_build()
}
pub fn default_code_inline_fn(args: CodeInlineFnArgs) -> impl UiNode {
let mut builder = text_view_builder(args.txt, args.style);
widget_set! {
&mut builder;
font_family = ["JetBrains Mono", "Consolas", "monospace"];
background_color = light_dark(rgb(0.95, 0.95, 0.95), rgb(0.05, 0.05, 0.05));
}
builder.widget_build()
}
pub fn default_link_fn(args: LinkFnArgs) -> impl UiNode {
if args.items.is_empty() {
NilUiNode.boxed()
} else {
let url = args.url;
let mut items = args.items;
let items = if items.len() == 1 {
items.remove(0)
} else {
Wrap! {
children = items;
}
.boxed()
};
Button! {
style_fn = LinkStyle!();
child = items;
on_click = hn!(|args: &ClickArgs| {
args.propagation().stop();
let link = WINDOW.info().get(WIDGET.id()).unwrap().interaction_path();
LINK_EVENT.notify(LinkArgs::now(url.clone(), link));
});
}
.boxed()
}
}
pub fn default_code_block_fn(args: CodeBlockFnArgs) -> impl UiNode {
if ["ansi", "console"].contains(&args.lang.as_str()) {
zng_wgt_ansi_text::AnsiText! {
txt = args.txt;
padding = 6;
corner_radius = 4;
background_color = light_dark(rgb(0.95, 0.95, 0.95), rgb(0.05, 0.05, 0.05));
}
.boxed()
} else {
Text! {
txt = args.txt;
padding = 6;
corner_radius = 4;
font_family = ["JetBrains Mono", "Consolas", "monospace"];
background_color = light_dark(rgb(0.95, 0.95, 0.95), rgb(0.05, 0.05, 0.05));
}
.boxed()
}
}
pub fn default_paragraph_fn(mut args: ParagraphFnArgs) -> impl UiNode {
if args.items.is_empty() {
NilUiNode.boxed()
} else if args.items.len() == 1 {
args.items.remove(0)
} else {
Wrap! {
children = args.items;
}
.boxed()
}
}
pub fn default_heading_fn(args: HeadingFnArgs) -> impl UiNode {
if args.items.is_empty() {
NilUiNode.boxed()
} else {
Wrap! {
access_role = AccessRole::Heading;
access::level = NonZeroU32::new(args.level as _).unwrap();
font_size = match args.level {
HeadingLevel::H1 => 2.em(),
HeadingLevel::H2 => 1.5.em(),
HeadingLevel::H3 => 1.4.em(),
HeadingLevel::H4 => 1.3.em(),
HeadingLevel::H5 => 1.2.em(),
HeadingLevel::H6 => 1.1.em(),
};
children = args.items;
anchor = args.anchor;
}
.boxed()
}
}
pub fn default_list_fn(args: ListFnArgs) -> impl UiNode {
if args.items.is_empty() {
NilUiNode.boxed()
} else {
Grid! {
grid::cell::at = grid::cell::AT_AUTO; access_role = AccessRole::List;
margin = (0, 0, 0, 1.em());
cells = args.items;
columns = ui_vec![
grid::Column!(),
grid::Column! {
width = 1.lft()
},
];
}
.boxed()
}
}
pub fn default_def_list_fn(args: DefListArgs) -> impl UiNode {
if args.items.is_empty() {
NilUiNode.boxed()
} else {
Stack! {
access_role = AccessRole::List;
direction = StackDirection::top_to_bottom();
spacing = PARAGRAPH_SPACING_VAR;
children = args.items;
}
.boxed()
}
}
pub fn default_def_list_item_title_fn(args: DefListItemTitleArgs) -> impl UiNode {
if args.items.is_empty() {
NilUiNode.boxed()
} else {
Wrap! {
access_role = AccessRole::Term;
children = args.items;
font_weight = FontWeight::BOLD;
}
.boxed()
}
}
pub fn default_def_list_item_definition_fn(args: DefListItemDefinitionArgs) -> impl UiNode {
if args.items.is_empty() {
NilUiNode.boxed()
} else {
Wrap! {
access_role = AccessRole::Definition;
children = args.items;
margin = (0, 2.em());
}
.boxed()
}
}
pub fn default_list_item_bullet_fn(args: ListItemBulletFnArgs) -> impl UiNode {
if let Some(checked) = args.checked {
Text! {
grid::cell::at = grid::cell::AT_AUTO;
align = Align::TOP;
txt = " ✓ ";
font_color = FONT_COLOR_VAR.map(move |c| if checked { *c } else { c.transparent() });
background_color = FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
corner_radius = 4;
scale = 0.8.fct();
offset = (-(0.1.fct()), 0);
}
.boxed()
} else if let Some(n) = args.num {
Text! {
grid::cell::at = grid::cell::AT_AUTO;
txt = formatx!("{n}. ");
align = Align::RIGHT;
}
.boxed()
} else {
match args.depth {
0 => Wgt! {
grid::cell::at = grid::cell::AT_AUTO;
align = Align::TOP;
size = (5, 5);
corner_radius = 5;
margin = (0.6.em(), 0.5.em(), 0, 0);
background_color = FONT_COLOR_VAR;
},
1 => Wgt! {
grid::cell::at = grid::cell::AT_AUTO;
align = Align::TOP;
size = (5, 5);
corner_radius = 5;
margin = (0.6.em(), 0.5.em(), 0, 0);
border = 1.px(), FONT_COLOR_VAR.map_into();
},
_ => Wgt! {
grid::cell::at = grid::cell::AT_AUTO;
align = Align::TOP;
size = (5, 5);
margin = (0.6.em(), 0.5.em(), 0, 0);
background_color = FONT_COLOR_VAR;
},
}
.boxed()
}
}
pub fn default_list_item_fn(args: ListItemFnArgs) -> impl UiNode {
let mut blocks = args.blocks;
let mut items = args.items;
if items.is_empty() {
if blocks.is_empty() {
return NilUiNode.boxed();
}
} else {
let r = if items.len() == 1 { items.remove(0) } else { Wrap!(items).boxed() };
blocks.insert(0, r);
}
if blocks.len() > 1 {
Stack! {
access_role = AccessRole::ListItem;
grid::cell::at = grid::cell::AT_AUTO;
direction = StackDirection::top_to_bottom();
children = blocks;
}
.boxed()
} else {
Container! {
access_role = AccessRole::ListItem;
grid::cell::at = grid::cell::AT_AUTO;
child = blocks.remove(0);
}
.boxed()
}
}
pub fn default_image_fn(args: ImageFnArgs) -> impl UiNode {
let tooltip_fn = if args.title.is_empty() {
wgt_fn!()
} else {
let title = args.title;
wgt_fn!(|_| Tip!(Text!(title.clone())))
};
let alt_txt = args.alt_txt;
let mut alt_items = args.alt_items;
if alt_items.is_empty() {
zng_wgt_image::Image! {
align = Align::TOP_LEFT;
tooltip_fn;
access::label = alt_txt;
source = args.source;
}
} else {
let alt_items = if alt_items.len() == 1 {
alt_items.remove(0)
} else {
Wrap! {
children = alt_items;
}
.boxed()
};
let alt_items = ArcNode::new(alt_items);
zng_wgt_image::Image! {
align = Align::TOP_LEFT;
source = args.source;
tooltip_fn;
zng_wgt_access::label = alt_txt;
img_error_fn = wgt_fn!(|_| { alt_items.take_on_init() });
}
}
}
pub fn default_rule_fn(_: RuleFnArgs) -> impl UiNode {
zng_wgt_rule_line::hr::Hr! {
opacity = 50.pct();
}
}
pub fn default_block_quote_fn(args: BlockQuoteFnArgs) -> impl UiNode {
if args.items.is_empty() {
NilUiNode.boxed()
} else {
Stack! {
direction = StackDirection::top_to_bottom();
spacing = PARAGRAPH_SPACING_VAR;
children = args.items;
corner_radius = 2;
background_color = if args.level < 3 {
FONT_COLOR_VAR.map(|c| c.with_alpha(5.pct())).boxed()
} else {
colors::BLACK.transparent().into_boxed_var()
};
border = {
widths: (0, 0, 0, 4u32.saturating_sub(args.level).max(1) as i32),
sides: FONT_COLOR_VAR.map(|c| BorderSides::solid(c.with_alpha(60.pct()))),
};
padding = 4;
}
.boxed()
}
}
pub fn default_table_fn(args: TableFnArgs) -> impl UiNode {
Grid! {
access_role = AccessRole::Table;
background_color = FONT_COLOR_VAR.map(|c| c.with_alpha(5.pct()));
border = 1, FONT_COLOR_VAR.map(|c| c.with_alpha(30.pct()).into());
align = Align::LEFT;
auto_grow_fn = wgt_fn!(|args: grid::AutoGrowFnArgs| {
grid::Row! {
border = (0, 0, 1, 0), FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()).into());
background_color = {
let alpha = if args.index % 2 == 0 {
5.pct()
} else {
0.pct()
};
FONT_COLOR_VAR.map(move |c| c.with_alpha(alpha))
};
when *#is_last {
border = 0, BorderStyle::Hidden;
}
}
});
columns = std::iter::repeat_with(|| grid::Column!{}.boxed()).take(args.columns.len()).collect::<UiVec>();
cells = args.cells;
}
}
pub fn default_table_cell_fn(args: TableCellFnArgs) -> impl UiNode {
if args.items.is_empty() {
NilUiNode.boxed()
} else if args.is_heading {
Wrap! {
access_role = AccessRole::Cell;
grid::cell::at = grid::cell::AT_AUTO;
font_weight = FontWeight::BOLD;
padding = 6;
child_align = args.col_align;
children = args.items;
}
.boxed()
} else {
Wrap! {
access_role = AccessRole::Cell;
grid::cell::at = grid::cell::AT_AUTO;
padding = 6;
child_align = args.col_align;
children = args.items;
}
.boxed()
}
}
pub fn default_panel_fn(args: PanelFnArgs) -> impl UiNode {
if args.items.is_empty() {
NilUiNode.boxed()
} else {
Stack! {
direction = StackDirection::top_to_bottom();
spacing = PARAGRAPH_SPACING_VAR;
children = args.items;
}
.boxed()
}
}
pub fn default_footnote_ref_fn(args: FootnoteRefFnArgs) -> impl UiNode {
let url = formatx!("#footnote-{}", args.label);
Button! {
style_fn = LinkStyle!();
font_size = 0.7.em();
offset = (0, (-0.5).em());
crate::anchor = formatx!("footnote-ref-{}", args.label);
child = Text!("[{}]", args.label);
on_click = hn!(|args: &ClickArgs| {
args.propagation().stop();
let link = WINDOW.info().get(WIDGET.id()).unwrap().interaction_path();
crate::LINK_EVENT.notify(crate::LinkArgs::now(url.clone(), link));
});
}
}
pub fn default_footnote_def_fn(args: FootnoteDefFnArgs) -> impl UiNode {
let mut items = args.items;
let items = if items.is_empty() {
NilUiNode.boxed()
} else if items.len() == 1 {
items.remove(0)
} else {
Stack! {
direction = StackDirection::top_to_bottom();
children = items;
}
.boxed()
};
let url_back = formatx!("#footnote-ref-{}", args.label);
Stack! {
direction = StackDirection::left_to_right();
spacing = 0.5.em();
anchor = formatx!("footnote-{}", args.label);
children = ui_vec![
Button! {
style_fn = LinkStyle!();
child = Text!("[^{}]", args.label);
on_click = hn!(|args: &ClickArgs| {
args.propagation().stop();
let link = WINDOW.info().get(WIDGET.id()).unwrap().interaction_path();
LINK_EVENT.notify(LinkArgs::now(url_back.clone(), link));
});
},
items,
];
}
}