use cranpose_ui::text::AnnotatedString;
use cranpose_ui::widgets::dialog::{DialogSpec, DismissReason};
use cranpose_ui::widgets::icon::{IconButtonSpec, IconSpec};
use cranpose_ui::widgets::scaffold::ScaffoldSpec;
use cranpose_ui::widgets::slider::SliderSpec;
use cranpose_ui::{measure_layout, run_test_composition};
use cranpose_ui::{Modifier, TextStyle};
use std::cell::Cell;
use std::rc::Rc;
const ICON: &str = "M0 0 L24 0 L24 24 L0 24 Z";
fn composed(content: impl FnMut() + 'static) {
let mut composition = run_test_composition(content);
let root = composition
.root()
.expect("the composition produced no root node");
let handle = composition.runtime_handle();
let mut applier = composition.applier_mut();
applier.set_runtime_handle(handle);
let measured = measure_layout(
&mut applier,
root,
cranpose_ui_graphics::Size::new(400.0, 800.0),
);
applier.clear_runtime_handle();
measured.expect("the tree measures");
}
fn composed_in_popup_host(content: impl Fn() + 'static) {
let content = std::rc::Rc::new(content);
composed(move || {
let content = std::rc::Rc::clone(&content);
cranpose_ui::widgets::popup::PopupHost(move || content());
});
}
#[test]
fn a_dialog_composes_its_content_over_a_scrim() {
let shown = Rc::new(Cell::new(0usize));
let counter = Rc::clone(&shown);
composed_in_popup_host(move || {
let counter = Rc::clone(&counter);
cranpose_ui::widgets::dialog::Dialog(
DialogSpec::default(),
|_reason: DismissReason| {},
move || counter.set(counter.get() + 1),
);
});
assert_eq!(shown.get(), 1, "the dialog did not compose its content");
}
#[test]
fn a_dialog_takes_the_scrim_colour_it_is_given() {
composed_in_popup_host(|| {
cranpose_ui::widgets::dialog::DialogWithScrim(
DialogSpec::default(),
cranpose_ui_graphics::Color::rgba(0.0, 0.0, 0.0, 0.5),
|_reason: DismissReason| {},
|| {},
);
});
}
#[test]
fn for_each_composes_one_row_per_item_under_its_own_key() {
let rows = Rc::new(std::cell::RefCell::new(Vec::new()));
let sink = Rc::clone(&rows);
run_test_composition(move || {
let sink = Rc::clone(&sink);
cranpose_ui::widgets::foreach::ForEach(&[1u32, 2, 3], move |item| {
sink.borrow_mut().push(*item);
});
});
assert_eq!(*rows.borrow(), vec![1, 2, 3]);
}
#[test]
fn an_icon_composes_from_a_vector_path() {
composed(|| {
cranpose_ui::widgets::icon::IconWith(Modifier::empty(), ICON, IconSpec::default(), None);
});
}
#[test]
fn an_icon_button_composes_its_content_and_carries_a_description() {
let drawn = Rc::new(Cell::new(0usize));
let counter = Rc::clone(&drawn);
composed(move || {
let counter = Rc::clone(&counter);
cranpose_ui::widgets::icon::IconButton(
Modifier::empty(),
"Close",
|| {},
move || counter.set(counter.get() + 1),
);
});
assert_eq!(drawn.get(), 1, "the button did not compose its icon");
}
#[test]
fn an_icon_button_accepts_a_spec_and_a_caller_owned_interaction_source() {
composed(|| {
let source = cranpose_ui::rememberMutableInteractionSource();
cranpose_ui::widgets::icon::IconButtonWith(
Modifier::empty(),
"Close",
IconButtonSpec::default(),
Some(source),
|| {},
|| {},
);
});
}
#[test]
fn an_icon_button_spec_carries_the_colours_it_is_given() {
let plain = IconButtonSpec::default();
let coloured = IconButtonSpec::default()
.with_colors(cranpose_ui::widgets::icon::IconButtonColors::default());
assert_eq!(
plain.enabled, coloured.enabled,
"colours must not disturb the rest of the spec"
);
}
#[test]
fn linked_text_composes_a_string_that_carries_a_link() {
let annotated = AnnotatedString::builder()
.push_link(cranpose_ui::text::LinkAnnotation::Url(
"https://example.invalid".to_string(),
))
.append("open me")
.pop()
.to_annotated_string();
composed(move || {
cranpose_ui::widgets::linked_text::LinkedText(
annotated.clone(),
Modifier::empty(),
TextStyle::default(),
|_url| {},
);
});
}
#[test]
fn a_scaffold_composes_both_bars_and_hands_its_content_the_insets() {
let slots = Rc::new(Cell::new(0usize));
let top = Rc::clone(&slots);
let bottom = Rc::clone(&slots);
let body = Rc::clone(&slots);
composed(move || {
let top = Rc::clone(&top);
let bottom = Rc::clone(&bottom);
let body = Rc::clone(&body);
cranpose_ui::widgets::scaffold::Scaffold(
Modifier::empty(),
move || top.set(top.get() + 1),
move || bottom.set(bottom.get() + 1),
move |_padding| body.set(body.get() + 1),
);
});
assert_eq!(slots.get(), 3, "a scaffold slot did not compose");
}
#[test]
fn a_scaffold_composes_its_floating_action_slot() {
let action = Rc::new(Cell::new(0usize));
let counter = Rc::clone(&action);
composed(move || {
let counter = Rc::clone(&counter);
cranpose_ui::widgets::scaffold::ScaffoldWith(
Modifier::empty(),
ScaffoldSpec::default(),
|| {},
|| {},
move || counter.set(counter.get() + 1),
|_padding| {},
);
});
assert_eq!(action.get(), 1, "the floating action slot did not compose");
}
#[test]
fn a_slider_hands_its_content_a_track_and_a_thumb_offset() {
let geometry = Rc::new(Cell::new((f32::NAN, f32::NAN)));
let sink = Rc::clone(&geometry);
composed(move || {
let sink = Rc::clone(&sink);
cranpose_ui::widgets::slider::Slider(
Modifier::empty().size(cranpose_ui_graphics::Size::new(200.0, 48.0)),
0.5,
|_value| {},
|| {},
SliderSpec::default(),
move |scope| sink.set((scope.track_extent(), scope.thumb_offset())),
);
});
let (track, thumb) = geometry.get();
assert!(track.is_finite(), "the slider reported no track extent");
assert!(thumb.is_finite(), "the slider reported no thumb offset");
assert!(
thumb >= 0.0 && thumb <= track.max(0.0),
"the thumb sits at {thumb} outside a track of {track}"
);
}
#[test]
fn a_swipe_to_dismiss_box_composes_its_content() {
let drawn = Rc::new(Cell::new(0usize));
let counter = Rc::clone(&drawn);
composed(move || {
let counter = Rc::clone(&counter);
cranpose_ui::widgets::swipe_to_dismiss::SwipeToDismissBox(
Modifier::empty(),
|| {},
move || counter.set(counter.get() + 1),
);
});
assert_eq!(drawn.get(), 1, "the box did not compose its content");
}
#[test]
fn a_swipe_dismiss_state_is_remembered_and_starts_at_rest() {
let offsets = Rc::new(std::cell::RefCell::new(Vec::new()));
let sink = Rc::clone(&offsets);
run_test_composition(move || {
let state = cranpose_ui::widgets::swipe_to_dismiss::rememberSwipeDismissState();
sink.borrow_mut().push(state.offset());
});
assert_eq!(
*offsets.borrow(),
vec![0.0],
"a swipe state nobody touched was not at rest"
);
}
#[test]
fn the_popup_viewport_local_answers_inside_a_composition() {
run_test_composition(|| {
let size = cranpose_ui::widgets::popup::local_popup_viewport()
.current()
.get();
assert!(
size.width >= 0.0 && size.height >= 0.0,
"the popup viewport reported a negative size"
);
});
}
#[test]
fn an_anchored_popup_composes_its_anchor_and_its_popup() {
let slots = Rc::new(Cell::new(0usize));
let anchor = Rc::clone(&slots);
let popup = Rc::clone(&slots);
composed_in_popup_host(move || {
let anchor = Rc::clone(&anchor);
let popup = Rc::clone(&popup);
cranpose_ui::widgets::popup::PopupAnchored(
Modifier::empty(),
true,
cranpose_ui_graphics::Point::new(0.0, 0.0),
move || anchor.set(anchor.get() + 1),
move || popup.set(popup.get() + 1),
);
});
assert!(
slots.get() >= 1,
"neither the anchor nor the popup composed"
);
}
#[test]
fn a_dismissable_popup_composes_its_content() {
composed_in_popup_host(|| {
cranpose_ui::widgets::popup::PopupDismissable(
cranpose_ui_graphics::Rect::from_size(cranpose_ui_graphics::Size::new(10.0, 10.0)),
cranpose_ui_graphics::Point::new(0.0, 0.0),
|| {},
|| {},
);
});
}
#[test]
fn a_conditionally_dismissable_popup_composes_its_content() {
composed_in_popup_host(|| {
cranpose_ui::widgets::popup::PopupDismissableWhen(
true,
cranpose_ui_graphics::Rect::from_size(cranpose_ui_graphics::Size::new(10.0, 10.0)),
cranpose_ui_graphics::Point::new(0.0, 0.0),
|| {},
|| {},
);
});
}
#[test]
fn a_vertical_scrollbar_composes_against_a_scroll_state() {
composed(|| {
let state = cranpose_ui::scroll::ScrollState::new(0.0);
cranpose_ui::widgets::scrollbar::VerticalScrollbar(
Modifier::empty().size(cranpose_ui_graphics::Size::new(8.0, 200.0)),
state,
);
});
}
#[test]
fn a_horizontal_scrollbar_composes_against_a_scroll_state() {
composed(|| {
let state = cranpose_ui::scroll::ScrollState::new(0.0);
cranpose_ui::widgets::scrollbar::HorizontalScrollbar(
Modifier::empty().size(cranpose_ui_graphics::Size::new(200.0, 8.0)),
state,
);
});
}