use std::cell::RefCell;
use std::rc::Rc;
pub struct Callback<T = ()> {
f: Rc<RefCell<dyn FnMut(T)>>,
}
impl<T> Clone for Callback<T> {
fn clone(&self) -> Self {
Callback { f: self.f.clone() }
}
}
impl<T> Callback<T> {
pub fn new(f: impl FnMut(T) + 'static) -> Callback<T> {
Callback {
f: Rc::new(RefCell::new(f)),
}
}
pub fn noop() -> Callback<T> {
Callback::new(|_| {})
}
pub fn call(&self, arg: T) {
match self.f.try_borrow_mut() {
Ok(mut f) => f(arg),
Err(_) => {
debug_assert!(
false,
"abstracttui Callback: re-entrant call of the same callback \
(a callback must not invoke itself; post work instead)"
);
}
}
}
}
impl<T> Default for Callback<T> {
fn default() -> Self {
Callback::noop()
}
}
impl<T, F: FnMut(T) + 'static> From<F> for Callback<T> {
fn from(f: F) -> Self {
Callback::new(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::Size;
use crate::layout::{Dimension, Style};
use crate::reactive::Scope;
use crate::theme::{default_theme, TokenSet};
use crate::ui::{text, Element, Role, View};
use crate::widgets::itest_util::{click, mount_widget, render};
use crate::widgets::Button;
struct CardProps {
title: String,
on_close: Callback<()>,
children: View,
}
fn card(cx: Scope, t: &TokenSet, props: CardProps) -> View {
let close = props.on_close.clone();
Element::new()
.role(Role::Region)
.access_label(props.title.clone())
.style(Style::column().width(Dimension::Percent(1.0)))
.child(
Element::new()
.style(Style::row().height(Dimension::Cells(1)))
.child(text(props.title))
.child(
Button::new("x")
.on_click(move || close.call(()))
.element(cx, t)
.build(),
)
.build(),
)
.child(props.children)
.build()
}
#[test]
fn card_component_composes_twice_with_props_events_and_slots() {
let t = default_theme().tokens;
let closed: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
let (c1, c2) = (closed.clone(), closed.clone());
let (root, mut tree) = mount_widget(Size::new(24, 6), |cx| {
Element::new()
.style(Style::column().width(Dimension::Percent(1.0)))
.child(card(
cx,
&t,
CardProps {
title: "Logs".into(),
on_close: Callback::new(move |()| c1.borrow_mut().push("logs")),
children: text("log body"),
},
))
.child(card(
cx,
&t,
CardProps {
title: "Stats".into(),
on_close: Callback::new(move |()| c2.borrow_mut().push("stats")),
children: text("stat body"),
},
))
.build()
});
let canvas = render(&mut tree, Size::new(24, 6));
let screen: Vec<String> = (0..6).map(|y| canvas.row_text(y)).collect();
assert!(screen[0].contains("Logs"), "{screen:?}");
assert!(screen[1].contains("log body"), "{screen:?}");
assert!(screen[2].contains("Stats"), "{screen:?}");
assert!(screen[3].contains("stat body"), "{screen:?}");
let x = screen[0].find('x').expect("close button visible") as i32;
click(&mut tree, x, 0);
let x2 = screen[2].find('x').expect("second close visible") as i32;
click(&mut tree, x2, 2);
assert_eq!(*closed.borrow(), vec!["logs", "stats"]);
let a11y = tree.accessibility_tree();
let labels: Vec<&str> = a11y
.entries
.iter()
.filter(|e| e.role == Role::Region)
.map(|e| e.label.as_str())
.collect();
assert_eq!(labels, vec!["Logs", "Stats"]);
root.dispose();
}
mod todo_app {
use crate::layout::{Dimension, Style};
use crate::prelude::*;
use crate::reactive::Scope;
use crate::theme::TokenSet;
use crate::widgets::{List, TextInput};
pub fn app(cx: Scope, t: &TokenSet) -> View {
let items = cx.signal(vec!["learn abstracttui".to_string()]);
let selected = cx.signal(0usize);
let draft = cx.signal(String::new());
let tokens = *t;
let input = TextInput::new()
.value(draft)
.placeholder("add a todo, press Enter")
.on_submit(move |s: &str| {
if !s.is_empty() {
let s = s.to_string();
items.update(|v| v.push(s));
draft.set(String::new());
}
})
.element(cx, t)
.build();
Element::new()
.style(
Style::column()
.width(Dimension::Percent(1.0))
.height(Dimension::Percent(1.0)),
)
.child(input)
.child(dyn_view_scoped(
Style::default().width(Dimension::Percent(1.0)).grow(1.0),
move |gen_cx| {
List::new(items.get())
.selection(selected)
.element(gen_cx, &tokens)
.build()
},
))
.child(dyn_view(
Style::default().height(Dimension::Cells(1)),
move || text(format!("{} todos", items.with(|v| v.len()))),
))
.build()
}
}
#[test]
fn sixty_line_app_proof_renders_and_reacts() {
let t = default_theme().tokens;
let (root, mut tree) = mount_widget(Size::new(30, 8), |cx| todo_app::app(cx, &t));
crate::widgets::itest_util::key(&mut tree, crate::ui::Key::Tab);
crate::widgets::itest_util::type_str(&mut tree, "write tests");
crate::widgets::itest_util::key(&mut tree, crate::ui::Key::Enter);
crate::reactive::flush_effects();
tree.layout();
let canvas = render(&mut tree, Size::new(30, 8));
let screen: String = (0..8).map(|y| canvas.row_text(y) + "\n").collect();
assert!(screen.contains("learn abstracttui"), "{screen}");
assert!(screen.contains("write tests"), "{screen}");
assert!(screen.contains("2 todos"), "{screen}");
root.dispose();
}
#[test]
fn one_component_reused_across_two_apps() {
let t = default_theme().tokens;
let build = move |title: &str, body: &'static str| {
let title = title.to_string();
mount_widget(Size::new(20, 4), move |cx| {
card(
cx,
&t,
CardProps {
title,
on_close: Callback::noop(),
children: text(body),
},
)
})
};
let (root_a, mut app_a) = build("App A", "alpha body");
let (root_b, mut app_b) = build("App B", "beta body");
let a = render(&mut app_a, Size::new(20, 4));
let b = render(&mut app_b, Size::new(20, 4));
assert!(a.row_text(0).contains("App A"));
assert!(a.row_text(1).contains("alpha body"));
assert!(b.row_text(0).contains("App B"));
assert!(b.row_text(1).contains("beta body"));
root_a.dispose();
root_b.dispose();
}
#[test]
fn callback_is_clone_shared_and_reentrancy_safe() {
let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let c = count.clone();
let cb: Callback<i32> = Callback::new(move |n| *c.borrow_mut() += n as u32);
let cb2 = cb.clone();
cb.call(2);
cb2.call(3);
assert_eq!(*count.borrow(), 5, "clones share one callback");
let optional: Callback<()> = Callback::default();
optional.call(()); }
}