use std::ops::RangeInclusive;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Tone {
#[default]
Normal,
Dim,
Accent,
Warning,
Critical,
Good,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Icon {
Volume { level: f32, muted: bool },
Wifi { strength: Option<f32> },
Wired,
Battery { charge: f32, charging: bool },
Lock,
Info,
Back,
Forward,
Check,
Refresh,
Close,
Reveal,
Bolt,
Plug,
Mic { muted: bool },
Speaker,
Headphones,
Display,
Bell { muted: bool },
}
pub struct Node<M>(Kind<M>);
#[doc(hidden)]
pub enum Kind<M> {
Label {
text: String,
tone: Option<Tone>,
},
Heading {
text: String,
tone: Option<Tone>,
},
Icon {
icon: Icon,
tone: Option<Tone>,
},
AppIcon {
app_id: String,
tone: Option<Tone>,
},
Counter {
value: i64,
range: RangeInclusive<i64>,
unit: String,
tone: Option<Tone>,
},
Alternate {
options: Vec<Node<M>>,
current: usize,
hovered: Option<usize>,
grows: bool,
},
Row(Vec<Node<M>>),
Column(Vec<Node<M>>),
Grid {
columns: usize,
cells: Vec<Node<M>>,
},
Button {
content: Box<Node<M>>,
on_press: M,
on_secondary: Option<M>,
prominent: bool,
},
Switcher {
options: Vec<(Node<M>, M)>,
current: Option<usize>,
},
Gulp(Box<Node<M>>),
Separator,
Empty,
ListItem {
title: String,
subtitle: Option<String>,
leading: Option<Box<Node<M>>>,
trailing: Option<Box<Node<M>>>,
selected: bool,
on_press: Option<M>,
on_secondary: Option<M>,
},
List(Vec<Node<M>>),
Section {
title: String,
trailing: Option<Box<Node<M>>>,
},
Header {
title: String,
on_back: Option<M>,
trailing: Option<Box<Node<M>>>,
},
Detail {
name: String,
value: Box<Node<M>>,
},
Fill,
Slider {
value: f32,
range: RangeInclusive<f32>,
step: f32,
on_change: Box<dyn Fn(f32) -> M>,
on_release: Option<M>,
},
Toggle {
on: bool,
on_toggle: M,
},
Meter {
fraction: f32,
limit: Option<f32>,
tone: Option<Tone>,
},
TextInput {
placeholder: String,
value: String,
on_input: Box<dyn Fn(String) -> M>,
on_submit: Option<M>,
secret: bool,
trailing: Option<Box<Node<M>>>,
tone: Option<Tone>,
},
Display(Box<Node<M>>),
Mark(Box<Node<M>>),
Badge {
text: String,
tone: Option<Tone>,
},
Tabs(Vec<(String, Node<M>)>),
}
pub fn label<M>(text: impl Into<String>) -> Node<M> {
Node(Kind::Label {
text: text.into(),
tone: None,
})
}
pub fn heading<M>(text: impl Into<String>) -> Node<M> {
Node(Kind::Heading {
text: text.into(),
tone: None,
})
}
pub fn icon<M>(icon: Icon) -> Node<M> {
Node(Kind::Icon { icon, tone: None })
}
pub fn app_icon<M>(app_id: impl Into<String>) -> Node<M> {
Node(Kind::AppIcon {
app_id: app_id.into(),
tone: None,
})
}
pub fn counter<M>(value: i64, range: RangeInclusive<i64>, unit: impl Into<String>) -> Node<M> {
Node(Kind::Counter {
value,
range,
unit: unit.into(),
tone: None,
})
}
pub fn alternate<M>(options: impl IntoIterator<Item = Node<M>>, current: usize) -> Node<M> {
Node(Kind::Alternate {
options: options.into_iter().collect(),
current,
hovered: None,
grows: false,
})
}
pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
alternate([normal, hovered], 0).on_hover(1)
}
pub fn expand<M>(collapsed: Node<M>, expanded: Node<M>) -> Node<M> {
Node(Kind::Alternate {
options: vec![collapsed, expanded],
current: 0,
hovered: Some(1),
grows: true,
})
}
pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
Node(Kind::Row(children.into_iter().collect()))
}
pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
Node(Kind::Column(children.into_iter().collect()))
}
pub fn grid<M>(columns: usize, cells: impl IntoIterator<Item = Node<M>>) -> Node<M> {
Node(Kind::Grid {
columns: columns.max(1),
cells: cells.into_iter().collect(),
})
}
pub fn button<M>(content: Node<M>, on_press: M) -> Node<M> {
Node(Kind::Button {
content: Box::new(content),
on_press,
on_secondary: None,
prominent: false,
})
}
pub fn switcher<M>(
options: impl IntoIterator<Item = (Node<M>, M)>,
current: Option<usize>,
) -> Node<M> {
Node(Kind::Switcher {
options: options.into_iter().collect(),
current,
})
}
pub fn gulp<M>(content: Node<M>) -> Node<M> {
Node(Kind::Gulp(Box::new(content)))
}
pub fn separator<M>() -> Node<M> {
Node(Kind::Separator)
}
pub fn empty<M>() -> Node<M> {
Node(Kind::Empty)
}
pub fn list_item<M>(title: impl Into<String>) -> Node<M> {
Node(Kind::ListItem {
title: title.into(),
subtitle: None,
leading: None,
trailing: None,
selected: false,
on_press: None,
on_secondary: None,
})
}
pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
Node(Kind::List(rows.into_iter().collect()))
}
pub fn section<M>(title: impl Into<String>) -> Node<M> {
Node(Kind::Section {
title: title.into(),
trailing: None,
})
}
pub fn header<M>(title: impl Into<String>) -> Node<M> {
Node(Kind::Header {
title: title.into(),
on_back: None,
trailing: None,
})
}
pub fn detail<M>(name: impl Into<String>, value: Node<M>) -> Node<M> {
Node(Kind::Detail {
name: name.into(),
value: Box::new(value),
})
}
pub fn fill<M>() -> Node<M> {
Node(Kind::Fill)
}
pub fn slider<M: 'static>(
value: f32,
range: RangeInclusive<f32>,
on_change: impl Fn(f32) -> M + 'static,
) -> Node<M> {
Node(Kind::Slider {
value,
range,
step: 1.0,
on_change: Box::new(on_change),
on_release: None,
})
}
pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
Node(Kind::Toggle { on, on_toggle })
}
pub fn meter<M>(fraction: f32) -> Node<M> {
Node(Kind::Meter {
fraction,
limit: None,
tone: None,
})
}
pub fn text_input<M: 'static>(
placeholder: impl Into<String>,
value: impl Into<String>,
on_input: impl Fn(String) -> M + 'static,
) -> Node<M> {
Node(Kind::TextInput {
placeholder: placeholder.into(),
value: value.into(),
on_input: Box::new(on_input),
on_submit: None,
secret: false,
trailing: None,
tone: None,
})
}
pub fn display<M>(content: Node<M>) -> Node<M> {
Node(Kind::Display(Box::new(content)))
}
pub fn mark<M>(content: Node<M>) -> Node<M> {
Node(Kind::Mark(Box::new(content)))
}
pub fn badge<M>(text: impl Into<String>) -> Node<M> {
Node(Kind::Badge {
text: text.into(),
tone: None,
})
}
pub fn tabs<M>(pages: impl IntoIterator<Item = (impl Into<String>, Node<M>)>) -> Node<M> {
Node(Kind::Tabs(
pages
.into_iter()
.map(|(name, page)| (name.into(), page))
.collect(),
))
}
impl<M> Node<M> {
pub fn tone(mut self, tone: Tone) -> Self {
self.set_tone(tone);
self
}
pub fn on_hover(mut self, index: usize) -> Self {
if let Kind::Alternate { hovered, .. } = &mut self.0 {
*hovered = Some(index);
}
self
}
pub fn on_secondary(mut self, message: M) -> Self {
if let Kind::Button { on_secondary, .. } | Kind::ListItem { on_secondary, .. } = &mut self.0
{
*on_secondary = Some(message);
}
self
}
pub fn prominent(mut self) -> Self {
if let Kind::Button { prominent, .. } = &mut self.0 {
*prominent = true;
}
self
}
pub fn on_press(mut self, message: M) -> Self {
if let Kind::ListItem { on_press, .. } = &mut self.0 {
*on_press = Some(message);
}
self
}
pub fn subtitle(mut self, text: impl Into<String>) -> Self {
if let Kind::ListItem { subtitle, .. } = &mut self.0 {
*subtitle = Some(text.into());
}
self
}
pub fn leading(mut self, node: Node<M>) -> Self {
if let Kind::ListItem { leading, .. } = &mut self.0 {
*leading = Some(Box::new(node));
}
self
}
pub fn trailing(mut self, node: Node<M>) -> Self {
if let Kind::ListItem { trailing, .. }
| Kind::Section { trailing, .. }
| Kind::Header { trailing, .. }
| Kind::TextInput { trailing, .. } = &mut self.0
{
*trailing = Some(Box::new(node));
}
self
}
pub fn selected(mut self, yes: bool) -> Self {
if let Kind::ListItem { selected, .. } = &mut self.0 {
*selected = yes;
}
self
}
pub fn on_back(mut self, message: M) -> Self {
if let Kind::Header { on_back, .. } = &mut self.0 {
*on_back = Some(message);
}
self
}
pub fn step(mut self, by: f32) -> Self {
if let Kind::Slider { step, .. } = &mut self.0 {
*step = by;
}
self
}
pub fn on_release(mut self, message: M) -> Self {
if let Kind::Slider { on_release, .. } = &mut self.0 {
*on_release = Some(message);
}
self
}
pub fn on_submit(mut self, message: M) -> Self {
if let Kind::TextInput { on_submit, .. } = &mut self.0 {
*on_submit = Some(message);
}
self
}
pub fn secret(mut self, yes: bool) -> Self {
if let Kind::TextInput { secret, .. } = &mut self.0 {
*secret = yes;
}
self
}
pub fn limit(mut self, at: f32) -> Self {
if let Kind::Meter { limit, .. } = &mut self.0 {
*limit = Some(at);
}
self
}
fn set_tone(&mut self, new: Tone) {
match &mut self.0 {
Kind::Label { tone, .. }
| Kind::Heading { tone, .. }
| Kind::Icon { tone, .. }
| Kind::AppIcon { tone, .. }
| Kind::Counter { tone, .. }
| Kind::Meter { tone, .. }
| Kind::TextInput { tone, .. }
| Kind::Badge { tone, .. } => {
tone.get_or_insert(new);
}
Kind::Alternate {
options: children, ..
}
| Kind::Row(children)
| Kind::Column(children)
| Kind::Grid {
cells: children, ..
}
| Kind::List(children) => {
children.iter_mut().for_each(|child| child.set_tone(new));
}
Kind::Button { content, .. }
| Kind::Gulp(content)
| Kind::Display(content)
| Kind::Mark(content)
| Kind::Detail { value: content, .. } => content.set_tone(new),
Kind::ListItem {
leading, trailing, ..
} => {
leading
.iter_mut()
.chain(trailing.iter_mut())
.for_each(|child| child.set_tone(new));
}
Kind::Tabs(pages) => {
for (_, page) in pages {
page.set_tone(new);
}
}
Kind::Switcher { options, .. } => {
options
.iter_mut()
.for_each(|(child, _)| child.set_tone(new));
}
Kind::Separator
| Kind::Empty
| Kind::Section { .. }
| Kind::Header { .. }
| Kind::Fill
| Kind::Slider { .. }
| Kind::Toggle { .. } => {}
}
}
pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Node<N>
where
M: 'static,
{
let map_all = |nodes: Vec<Node<M>>| nodes.into_iter().map(|node| node.map(f)).collect();
let map_box = |node: Option<Box<Node<M>>>| node.map(|node| Box::new(node.map(f)));
Node(match self.0 {
Kind::Label { text, tone } => Kind::Label { text, tone },
Kind::Heading { text, tone } => Kind::Heading { text, tone },
Kind::Icon { icon, tone } => Kind::Icon { icon, tone },
Kind::AppIcon { app_id, tone } => Kind::AppIcon { app_id, tone },
Kind::Counter {
value,
range,
unit,
tone,
} => Kind::Counter {
value,
range,
unit,
tone,
},
Kind::Alternate {
options,
current,
hovered,
grows,
} => Kind::Alternate {
options: map_all(options),
current,
hovered,
grows,
},
Kind::Row(children) => Kind::Row(map_all(children)),
Kind::Column(children) => Kind::Column(map_all(children)),
Kind::Grid { columns, cells } => Kind::Grid {
columns,
cells: map_all(cells),
},
Kind::Button {
content,
on_press,
on_secondary,
prominent,
} => Kind::Button {
content: Box::new(content.map(f)),
on_press: f(on_press),
on_secondary: on_secondary.map(f),
prominent,
},
Kind::Switcher { options, current } => Kind::Switcher {
options: options
.into_iter()
.map(|(node, message)| (node.map(f), f(message)))
.collect(),
current,
},
Kind::Gulp(content) => Kind::Gulp(Box::new(content.map(f))),
Kind::Separator => Kind::Separator,
Kind::Empty => Kind::Empty,
Kind::ListItem {
title,
subtitle,
leading,
trailing,
selected,
on_press,
on_secondary,
} => Kind::ListItem {
title,
subtitle,
leading: map_box(leading),
trailing: map_box(trailing),
selected,
on_press: on_press.map(f),
on_secondary: on_secondary.map(f),
},
Kind::List(rows) => Kind::List(map_all(rows)),
Kind::Section { title, trailing } => Kind::Section {
title,
trailing: map_box(trailing),
},
Kind::Header {
title,
on_back,
trailing,
} => Kind::Header {
title,
on_back: on_back.map(f),
trailing: map_box(trailing),
},
Kind::Detail { name, value } => Kind::Detail {
name,
value: Box::new(value.map(f)),
},
Kind::Fill => Kind::Fill,
Kind::Slider {
value,
range,
step,
on_change,
on_release,
} => {
let g = f.clone();
Kind::Slider {
value,
range,
step,
on_change: Box::new(move |value| g(on_change(value))),
on_release: on_release.map(f),
}
}
Kind::Toggle { on, on_toggle } => Kind::Toggle {
on,
on_toggle: f(on_toggle),
},
Kind::Meter {
fraction,
limit,
tone,
} => Kind::Meter {
fraction,
limit,
tone,
},
Kind::TextInput {
placeholder,
value,
on_input,
on_submit,
secret,
trailing,
tone,
} => {
let g = f.clone();
Kind::TextInput {
placeholder,
value,
on_input: Box::new(move |text| g(on_input(text))),
on_submit: on_submit.map(f),
secret,
trailing: map_box(trailing),
tone,
}
}
Kind::Display(content) => Kind::Display(Box::new(content.map(f))),
Kind::Mark(content) => Kind::Mark(Box::new(content.map(f))),
Kind::Badge { text, tone } => Kind::Badge { text, tone },
Kind::Tabs(pages) => Kind::Tabs(
pages
.into_iter()
.map(|(name, page)| (name, page.map(f)))
.collect(),
),
})
}
#[doc(hidden)]
pub fn into_kind(self) -> Kind<M> {
self.0
}
#[doc(hidden)]
pub fn kind(&self) -> &Kind<M> {
&self.0
}
}
pub struct Item<M> {
content: Option<Node<M>>,
on_press: Option<M>,
on_secondary: Option<M>,
on_scroll: Option<(M, M)>,
}
impl<M> Item<M> {
pub fn new(content: Node<M>) -> Self {
Self {
content: Some(content),
on_press: None,
on_secondary: None,
on_scroll: None,
}
}
pub fn hidden() -> Self {
Self {
content: None,
on_press: None,
on_secondary: None,
on_scroll: None,
}
}
pub fn on_press(mut self, message: M) -> Self {
self.on_press = Some(message);
self
}
pub fn on_secondary(mut self, message: M) -> Self {
self.on_secondary = Some(message);
self
}
pub fn on_scroll(mut self, up: M, down: M) -> Self {
self.on_scroll = Some((up, down));
self
}
pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Item<N>
where
M: 'static,
{
Item {
content: self.content.map(|content| content.map(f)),
on_press: self.on_press.map(f),
on_secondary: self.on_secondary.map(f),
on_scroll: self.on_scroll.map(|(up, down)| (f(up), f(down))),
}
}
#[doc(hidden)]
pub fn into_parts(self) -> ItemParts<M> {
ItemParts {
content: self.content,
on_press: self.on_press,
on_secondary: self.on_secondary,
on_scroll: self.on_scroll,
}
}
}
#[doc(hidden)]
pub struct ItemParts<M> {
pub content: Option<Node<M>>,
pub on_press: Option<M>,
pub on_secondary: Option<M>,
pub on_scroll: Option<(M, M)>,
}
pub struct Panel<M> {
content: Node<M>,
on_open: Option<M>,
on_close: Option<M>,
}
impl<M> Panel<M> {
pub fn new(content: Node<M>) -> Self {
Self {
content,
on_open: None,
on_close: None,
}
}
pub fn on_open(mut self, message: M) -> Self {
self.on_open = Some(message);
self
}
pub fn on_close(mut self, message: M) -> Self {
self.on_close = Some(message);
self
}
pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Panel<N>
where
M: 'static,
{
Panel {
content: self.content.map(f),
on_open: self.on_open.map(f),
on_close: self.on_close.map(f),
}
}
#[doc(hidden)]
pub fn into_node(self) -> Node<M> {
self.content
}
#[doc(hidden)]
pub fn on_open_message(self) -> Option<M> {
self.on_open
}
#[doc(hidden)]
pub fn on_close_message(self) -> Option<M> {
self.on_close
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Urgency {
Low,
#[default]
Normal,
Critical,
}
pub struct Notice<M> {
key: u64,
summary: String,
app: Option<String>,
app_icon: Option<String>,
body: Option<String>,
urgency: Urgency,
actions: Vec<(String, M)>,
on_press: Option<M>,
on_dismiss: Option<M>,
expires: Option<(std::time::Duration, M)>,
reply: Option<Reply<M>>,
}
pub struct Reply<M> {
placeholder: String,
value: String,
on_input: std::rc::Rc<dyn Fn(String) -> M>,
on_submit: Option<M>,
on_cancel: Option<M>,
}
impl<M> Reply<M> {
pub fn new(
placeholder: impl Into<String>,
value: impl Into<String>,
on_input: impl Fn(String) -> M + 'static,
) -> Self {
Self {
placeholder: placeholder.into(),
value: value.into(),
on_input: std::rc::Rc::new(on_input),
on_submit: None,
on_cancel: None,
}
}
pub fn on_submit(mut self, message: M) -> Self {
self.on_submit = Some(message);
self
}
pub fn on_cancel(mut self, message: M) -> Self {
self.on_cancel = Some(message);
self
}
pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
where
M: 'static,
{
let g = f.clone();
let on_input = self.on_input;
Reply {
placeholder: self.placeholder,
value: self.value,
on_input: std::rc::Rc::new(move |text| g(on_input(text))),
on_submit: self.on_submit.map(f),
on_cancel: self.on_cancel.map(f),
}
}
#[doc(hidden)]
pub fn into_parts(self) -> ReplyParts<M> {
ReplyParts {
placeholder: self.placeholder,
value: self.value,
on_input: self.on_input,
on_submit: self.on_submit,
on_cancel: self.on_cancel,
}
}
}
#[doc(hidden)]
#[derive(Clone)]
pub struct ReplyParts<M> {
pub placeholder: String,
pub value: String,
pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
pub on_submit: Option<M>,
pub on_cancel: Option<M>,
}
impl<M> Notice<M> {
pub fn new(key: u64, summary: impl Into<String>) -> Self {
Self {
key,
summary: summary.into(),
app: None,
app_icon: None,
body: None,
urgency: Urgency::Normal,
actions: Vec::new(),
on_press: None,
on_dismiss: None,
expires: None,
reply: None,
}
}
pub fn app(mut self, name: impl Into<String>) -> Self {
self.app = Some(name.into());
self
}
pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
self.app_icon = Some(app_id.into());
self
}
pub fn body(mut self, text: impl Into<String>) -> Self {
self.body = Some(text.into());
self
}
pub fn urgency(mut self, urgency: Urgency) -> Self {
self.urgency = urgency;
self
}
pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
self.actions.push((label.into(), message));
self
}
pub fn on_press(mut self, message: M) -> Self {
self.on_press = Some(message);
self
}
pub fn on_dismiss(mut self, message: M) -> Self {
self.on_dismiss = Some(message);
self
}
pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
self.expires = Some((after, message));
self
}
pub fn reply(mut self, reply: Reply<M>) -> Self {
self.reply = Some(reply);
self
}
pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
where
M: 'static,
{
Notice {
key: self.key,
summary: self.summary,
app: self.app,
app_icon: self.app_icon,
body: self.body,
urgency: self.urgency,
actions: self
.actions
.into_iter()
.map(|(label, message)| (label, f(message)))
.collect(),
on_press: self.on_press.map(f),
on_dismiss: self.on_dismiss.map(f),
expires: self.expires.map(|(after, message)| (after, f(message))),
reply: self.reply.map(|reply| reply.map(f)),
}
}
pub fn key(&self) -> u64 {
self.key
}
#[doc(hidden)]
pub fn into_parts(self) -> NoticeParts<M> {
NoticeParts {
key: self.key,
summary: self.summary,
app: self.app,
app_icon: self.app_icon,
body: self.body,
urgency: self.urgency,
actions: self.actions,
on_press: self.on_press,
on_dismiss: self.on_dismiss,
expires: self.expires,
reply: self.reply.map(Reply::into_parts),
}
}
}
#[doc(hidden)]
#[derive(Clone)]
pub struct NoticeParts<M> {
pub key: u64,
pub summary: String,
pub app: Option<String>,
pub app_icon: Option<String>,
pub body: Option<String>,
pub urgency: Urgency,
pub actions: Vec<(String, M)>,
pub on_press: Option<M>,
pub on_dismiss: Option<M>,
pub expires: Option<(std::time::Duration, M)>,
pub reply: Option<ReplyParts<M>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn map_converts_every_message() {
let node = row([
button(label("a"), 1).on_secondary(4),
column([button(label("b"), 2), label("c")]),
switcher([(label("d"), 3)], Some(0)),
]);
let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
panic!("row became something else");
};
let mut messages = Vec::new();
collect(children, &mut messages);
assert_eq!(messages, [10, 40, 20, 30]);
}
fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
for node in nodes {
match node.into_kind() {
Kind::Button {
on_press,
on_secondary,
..
} => {
out.push(on_press);
out.extend(on_secondary);
}
Kind::Switcher { options, .. } => {
out.extend(options.into_iter().map(|(_, message)| message))
}
Kind::Row(children) | Kind::Column(children) => collect(children, out),
_ => {}
}
}
}
#[test]
fn hover_shows_the_second_option() {
let node: Node<()> = hover(label("a"), label("b"));
assert!(matches!(
node.into_kind(),
Kind::Alternate {
current: 0,
hovered: Some(1),
..
}
));
let node: Node<()> = label("a").on_hover(1);
assert!(matches!(node.into_kind(), Kind::Label { .. }));
}
#[test]
fn tone_fills_only_unset_text() {
let node: Node<()> =
row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
let Kind::Row(children) = node.into_kind() else {
unreachable!()
};
let tones: Vec<_> = children
.into_iter()
.map(|child| match child.into_kind() {
Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
_ => None,
})
.collect();
assert_eq!(
tones,
[Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
);
}
#[test]
fn map_reaches_new_nodes_and_their_closures() {
let node = column([
header("h").on_back(1).trailing(toggle(true, 2)),
list([list_item("a")
.on_press(3)
.on_secondary(4)
.trailing(button(icon(Icon::Info), 5))]),
slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
]);
let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
panic!("column became something else");
};
let mut children = children.into_iter().map(Node::into_kind);
let Some(Kind::Header {
on_back: Some(10),
trailing: Some(trailing),
..
}) = children.next()
else {
panic!("header lost its back message");
};
assert!(matches!(
trailing.into_kind(),
Kind::Toggle { on_toggle: 20, .. }
));
let Some(Kind::List(rows)) = children.next() else {
panic!("list lost");
};
let Some(Kind::ListItem {
on_press: Some(30),
on_secondary: Some(40),
trailing: Some(trailing),
..
}) = rows.into_iter().next().map(Node::into_kind)
else {
panic!("list item lost its messages");
};
assert!(matches!(
trailing.into_kind(),
Kind::Button { on_press: 50, .. }
));
let Some(Kind::Slider {
on_change,
on_release: Some(60),
..
}) = children.next()
else {
panic!("slider lost");
};
assert_eq!(on_change(0.3), 30);
let Some(Kind::TextInput {
on_input,
on_submit: Some(70),
..
}) = children.next()
else {
panic!("text input lost");
};
assert_eq!(on_input("abcd".into()), 40);
}
#[test]
fn tabs_map_and_tone_every_page() {
let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
.tone(Tone::Dim)
.map(&|n: i32| n * 10);
let Kind::Tabs(pages) = node.into_kind() else {
panic!("tabs became something else");
};
let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
assert_eq!(names, ["a", "b"]);
let mut messages = Vec::new();
for (_, page) in pages {
let Kind::Button {
content, on_press, ..
} = page.into_kind()
else {
panic!("page lost its button");
};
assert!(matches!(
content.into_kind(),
Kind::Label {
tone: Some(Tone::Dim),
..
}
));
messages.push(on_press);
}
assert_eq!(messages, [10, 20]);
}
#[test]
fn notice_map_reaches_every_message() {
let notice = Notice::new(7, "s")
.action("a", 1)
.action("b", 2)
.on_press(3)
.on_dismiss(4)
.expires(std::time::Duration::from_secs(5), 5)
.reply(
Reply::new("p", "v", |text: String| text.len() as i32)
.on_submit(6)
.on_cancel(7),
)
.map(&|n: i32| n * 10);
assert_eq!(notice.key(), 7);
let parts = notice.into_parts();
let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
assert_eq!(actions, [10, 20]);
assert_eq!(parts.on_press, Some(30));
assert_eq!(parts.on_dismiss, Some(40));
assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
let reply = parts.reply.unwrap();
assert_eq!((reply.on_input)("abc".into()), 30);
assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
}
#[test]
fn modifiers_only_touch_their_nodes() {
let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
assert!(matches!(node.into_kind(), Kind::Label { .. }));
let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
assert!(matches!(
node.into_kind(),
Kind::Button {
on_press: 1,
prominent: true,
..
}
));
}
#[test]
fn tone_reaches_details_and_row_icons() {
let node: Node<()> = column([
detail("n", label("v")),
list_item("t").leading(icon(Icon::Lock)),
])
.tone(Tone::Good);
let Kind::Column(children) = node.into_kind() else {
unreachable!()
};
let mut children = children.into_iter().map(Node::into_kind);
let Some(Kind::Detail { value, .. }) = children.next() else {
unreachable!()
};
assert!(matches!(
value.into_kind(),
Kind::Label {
tone: Some(Tone::Good),
..
}
));
let Some(Kind::ListItem {
leading: Some(leading),
..
}) = children.next()
else {
unreachable!()
};
assert!(matches!(
leading.into_kind(),
Kind::Icon {
tone: Some(Tone::Good),
..
}
));
}
}