Skip to content

Instantly share code, notes, and snippets.

@PsichiX
Last active December 5, 2020 03:31
Show Gist options
  • Save PsichiX/1b649b726049a0839c4e53e46b1c6f05 to your computer and use it in GitHub Desktop.
Save PsichiX/1b649b726049a0839c4e53e46b1c6f05 to your computer and use it in GitHub Desktop.
RAUI data oriented API (declarative UI)
#[test]
fn test_hello_world() {
// convenient macro that produces widget component processing function.
widget_component! {
// <component name> ( [list of context data to unpack into scope] )
app(key, named_slots) {
// easy way to get widgets from named slots.
unpack_named_slots!(named_slots => { title, content });
// we always return new widgets tree.
widget! {
// Forgive me the syntax, i'll make a JSX-like one soon using procedural macros.
// `#{key}` - provided value gives a unique name to node. keys allows widgets
// to save state between render calls. here we just pass key of this widget.
// `vertical_box` - name of widget component to use.
// `[...]` - listed widget slots. here we just put previously unpacked named slots.
(#{key} vertical_box [
{title}
{content}
])
}
}
}
widget_component! {
button(key, props, unmounter, phase) {
println!("=== PROCESS BUTTON: {} | PHASE: {:?}", key, phase);
// buttons use string as props data.
let label = props.read_cloned_or_default::<String>();
let k = key.to_string();
// you use unmounter for storing closures that will be called when widget will be
// unmounted from the widget tree.
// closure provides arguments such as:
// - widget id
// - widget state
// - message sender (this one is used to message other widgets you know about)
// - signal sender (this one is used to message application host)
unmounter.listen(move |_, _, _, _| {
println!("=== BUTTON UNMOUNTED: {}", k);
});
widget!{
(#{key} text: {label})
}
}
}
widget_component! {
title_bar(key, props) {
let title = props.read_cloned_or_default::<String>();
widget! {
(#{key} text: {title})
}
}
}
widget_component! {
vertical_box(key, listed_slots) {
// listed slots are just widget node children.
// here we just unwrap widget units (final atomic UI elements that renderers read).
let items = listed_slots
.into_iter()
.map(|slot| ListBoxItem {
slot: slot.try_into().expect("Cannot convert slot to WidgetUnit!"),
..Default::default()
})
.collect::<Vec<_>>();
// we use `{{{ ... }}}` to inform macro that this is widget unit.
widget! {{{
ListBox {
items,
..Default::default()
}
}}}
}
}
widget_component! {
text(key, props) {
let text = props.read_cloned_or_default::<String>();
widget!{{{
TextBox {
text,
..Default::default()
}
}}}
}
}
let mut application = Application::new();
let tree = widget! {
(app {
// <named slot name> = ( <widget to put in a slot> )
title = (title_bar: {"Hello".to_owned()})
content = (vertical_box [
(#{"hi"} button: {"Say hi!".to_owned()})
(#{"exit"} button: {"Close".to_owned()})
])
})
};
println!("=== INPUT:\n{:#?}", tree);
// some dummy widget tree renderer.
// it reads widget unit tree and transforms it into target format.
let mut renderer = HtmlRenderer::default();
println!("=== PROCESS");
// `apply()` sets new widget tree.
application.apply(tree);
// `render()` calls renderer to perform transformations on processed application widget tree.
if let Ok(output) = application.render(&mut renderer) {
println!("=== OUTPUT:\n{}", output);
}
println!("=== PROCESS");
// by default application won't process widget tree if nothing was changed.
// "change" is either any widget state change, or new message sent to any widget (messages
// can be sent from application host, for example a mouse click, or from another widget).
application.forced_process();
if let Ok(output) = application.render(&mut renderer) {
println!("=== OUTPUT:\n{}", output);
}
let tree = widget! {
(app)
};
println!("=== INPUT:\n{:#?}", tree);
println!("=== PROCESS");
application.apply(tree);
if let Ok(output) = application.render(&mut HtmlRenderer::default()) {
println!("=== OUTPUT:\n{}", output);
}
}
=== INPUT:
Component(
WidgetComponent {
type_name: "app",
named_slots: {
"title": Component(
WidgetComponent {
type_name: "title_bar",
},
),
"content": Component(
WidgetComponent {
type_name: "vertical_box",
listed_slots: [
Component(
WidgetComponent {
type_name: "button",
key: "hi",
},
),
Component(
WidgetComponent {
type_name: "button",
key: "exit",
},
),
],
},
),
},
},
)
=== PROCESS
=== PROCESS BUTTON: hi | PHASE: Mount
=== PROCESS BUTTON: exit | PHASE: Mount
=== OUTPUT:
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head >
<meta charset="utf-8">
</head>
<body >
<div >
<span >
Hello
</span>
<div >
<span >
Say hi!
</span>
<span >
Close
</span>
</div>
</div>
</body>
</html>
=== PROCESS
=== PROCESS BUTTON: hi | PHASE: Update
=== PROCESS BUTTON: exit | PHASE: Update
=== OUTPUT:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head >
<meta charset="utf-8">
</head>
<body >
<div >
<span >
Hello
</span>
<div >
<span >
Say hi!
</span>
<span >
Close
</span>
</div>
</div>
</body>
</html>
=== INPUT:
Component(
WidgetComponent {
type_name: "app",
},
)
=== PROCESS
=== BUTTON UNMOUNTED: hi
=== BUTTON UNMOUNTED: exit
=== OUTPUT:
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head >
<meta charset="utf-8">
</head>
<body >
<div >
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment