use kui_core::{
Align, Color, Core, CursorShape, EditKey, FloatConfig, InputEvent, Mods, MouseButton, NodeSpec,
QuadKind, Size, Vec2,
};
const VIEW: Size = Size { w: 400.0, h: 200.0 };
const RED: Color = Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
const GREEN: Color = Color {
r: 0.0,
g: 1.0,
b: 0.0,
a: 1.0,
};
const BLUE: Color = Color {
r: 0.0,
g: 0.0,
b: 1.0,
a: 1.0,
};
const GREY: Color = Color {
r: 0.2,
g: 0.2,
b: 0.2,
a: 1.0,
};
fn painted(core: &mut Core) -> Vec<&'static str> {
let (dl, _) = core.output();
dl.quads
.iter()
.filter(|q| q.kind == QuadKind::Solid)
.map(|q| {
if q.color == RED {
"red"
} else if q.color == GREEN {
"green"
} else if q.color == BLUE {
"blue"
} else if q.color == GREY {
"grey"
} else if q.rect.w == 4.0 || q.rect.w == 6.0 {
"bar"
} else if q.color.a == 0.0 && q.border_w > 0.0 {
"ring"
} else {
"?"
}
})
.collect()
}
fn float_at(x: f32, bg: Color) -> NodeSpec {
NodeSpec::column().size(200.0, 100.0).bg(bg).float(
FloatConfig::viewport()
.inside(Align::Start, Align::Start)
.offset(x, 50.0),
)
}
fn scroller_and_float(core: &mut Core, modal: bool) {
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed("list", NodeSpec::column().fill().scroll_y(), |ui| {
for i in 0..20 {
ui.leaf_keyed(
&format!("row{i}"),
NodeSpec::row().grow_width().height(30.0).bg(GREY),
);
}
});
let mut spec = float_at(250.0, RED)
.on_click("menu")
.cursor(CursorShape::Pointer);
if modal {
spec = spec.modal("menu");
}
ui.leaf_keyed("menu", spec);
ui.finish();
}
#[test]
fn a_scrollers_bar_paints_under_a_float_over_it() {
for modal in [false, true] {
let mut core = Core::new();
scroller_and_float(&mut core, modal);
let order = painted(&mut core);
let bar = order.iter().position(|n| *n == "bar").unwrap();
let menu = order.iter().position(|n| *n == "red").unwrap();
assert!(
bar < menu,
"modal={modal}: the bar is the in-flow layer's chrome, under the float: {order:?}"
);
}
}
#[test]
fn a_press_on_a_float_over_the_bar_reaches_the_float() {
let mut core = Core::new();
scroller_and_float(&mut core, false);
let list = core.key_of("list").unwrap();
core.handle_input(InputEvent::CursorMoved(Vec2::new(395.0, 100.0)));
assert_eq!(core.cursor_shape(), CursorShape::Pointer, "the float's");
core.handle_input(InputEvent::MouseDown {
button: MouseButton::Primary,
clicks: 1,
});
let out = core.handle_input(InputEvent::MouseUp {
button: MouseButton::Primary,
});
assert_eq!(out.len(), 1, "one click: {out:?}");
assert_eq!(out[0].payload.as_str(), Some("menu"));
scroller_and_float(&mut core, false);
assert_eq!(
core.scroll_offset(list).y,
0.0,
"the track was never pressed"
);
core.handle_input(InputEvent::CursorMoved(Vec2::new(395.0, 180.0)));
assert_eq!(core.cursor_shape(), CursorShape::Default);
core.handle_input(InputEvent::MouseDown {
button: MouseButton::Primary,
clicks: 1,
});
core.handle_input(InputEvent::MouseUp {
button: MouseButton::Primary,
});
scroller_and_float(&mut core, false);
assert!(core.scroll_offset(list).y > 0.0, "the track jumped");
}
fn bar_and_body(core: &mut Core, dropdown: bool, tooltip: bool) {
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed("bar", NodeSpec::row().height(24.0), |ui| {
if dropdown {
ui.leaf_keyed("dropdown", float_at(100.0, RED));
}
});
ui.leaf_keyed(
"btn",
NodeSpec::row()
.size(80.0, 30.0)
.bg(GREEN)
.on_click("btn")
.focusable(),
);
if tooltip {
ui.leaf_keyed("tip", float_at(150.0, BLUE));
}
ui.finish();
}
#[test]
fn floats_stack_in_the_order_they_opened() {
let mut core = Core::new();
bar_and_body(&mut core, true, true);
assert_eq!(painted(&mut core), ["green", "red", "blue"]);
bar_and_body(&mut core, true, false);
assert_eq!(painted(&mut core), ["green", "red"]);
bar_and_body(&mut core, true, true);
assert_eq!(painted(&mut core), ["green", "red", "blue"]);
bar_and_body(&mut core, false, true);
assert_eq!(painted(&mut core), ["green", "blue"]);
bar_and_body(&mut core, true, true);
assert_eq!(
painted(&mut core),
["green", "blue", "red"],
"the dropdown reopened over the tooltip"
);
bar_and_body(&mut core, true, true);
assert_eq!(painted(&mut core), ["green", "blue", "red"]);
}
#[test]
fn the_hit_list_is_the_stack() {
let mut core = Core::new();
let build = |core: &mut Core, dropdown: bool| {
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
if dropdown {
ui.leaf_keyed("dropdown", float_at(100.0, RED).on_click("dropdown"));
}
ui.leaf_keyed("tip", float_at(150.0, BLUE).on_click("tip"));
ui.finish();
};
let click = |core: &mut Core| -> Option<String> {
core.handle_input(InputEvent::CursorMoved(Vec2::new(200.0, 100.0)));
core.handle_input(InputEvent::MouseDown {
button: MouseButton::Primary,
clicks: 1,
});
core.handle_input(InputEvent::MouseUp {
button: MouseButton::Primary,
})
.pop()
.and_then(|e| e.payload.as_str().map(str::to_string))
};
build(&mut core, true);
assert_eq!(click(&mut core).as_deref(), Some("tip"), "tree order");
build(&mut core, false);
build(&mut core, true);
assert_eq!(
click(&mut core).as_deref(),
Some("dropdown"),
"reopened over the tooltip, so it takes the press"
);
}
#[test]
fn the_ring_paints_at_the_end_of_its_layer() {
let mut core = Core::new();
bar_and_body(&mut core, true, true);
let btn = core.key_of("btn").unwrap();
core.set_focus(Some(btn));
core.handle_input(InputEvent::Key(EditKey::Tab, Mods::default()));
core.set_focus(Some(btn));
bar_and_body(&mut core, true, true);
assert_eq!(
painted(&mut core),
["green", "ring", "red", "blue"],
"the in-flow layer's ring, under every float"
);
}
#[test]
fn a_ring_inside_a_float_ends_that_layer() {
let mut core = Core::new();
let build = |core: &mut Core| {
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed("popover", float_at(100.0, RED), |ui| {
ui.leaf_keyed(
"ok",
NodeSpec::row()
.size(40.0, 20.0)
.bg(GREEN)
.on_click("ok")
.focusable(),
);
});
ui.leaf_keyed("tip", float_at(150.0, BLUE));
ui.finish();
};
build(&mut core);
let ok = core.key_of("ok").unwrap();
core.set_focus(Some(ok));
core.handle_input(InputEvent::Key(EditKey::Tab, Mods::default()));
core.set_focus(Some(ok));
build(&mut core);
assert_eq!(painted(&mut core), ["red", "green", "ring", "blue"]);
}
#[test]
fn a_scroller_inside_a_float_bars_at_that_layers_end() {
let mut core = Core::new();
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed("menu", float_at(100.0, RED).scroll_y(), |ui| {
for i in 0..10 {
ui.leaf_keyed(
&format!("row{i}"),
NodeSpec::row().grow_width().height(30.0).bg(GREY),
);
}
});
ui.leaf_keyed("tip", float_at(150.0, BLUE));
ui.finish();
let order = painted(&mut core);
let bar = order.iter().position(|n| *n == "bar").unwrap();
let last_row = order.iter().rposition(|n| *n == "grey").unwrap();
let tip = order.iter().position(|n| *n == "blue").unwrap();
assert!(last_row < bar && bar < tip, "{order:?}");
}
#[test]
fn a_nested_float_is_above_the_float_it_is_in() {
let mut core = Core::new();
let build = |core: &mut Core, sub: bool| {
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed("menu", float_at(100.0, RED), |ui| {
if sub {
ui.leaf_keyed("submenu", float_at(120.0, BLUE));
}
ui.leaf_keyed("item", NodeSpec::row().size(40.0, 20.0).bg(GREEN));
});
ui.finish();
};
build(&mut core, true);
assert_eq!(
painted(&mut core),
["red", "green", "blue"],
"the submenu is not painted where it sits in the menu's subtree"
);
build(&mut core, false);
build(&mut core, true);
assert_eq!(painted(&mut core), ["red", "green", "blue"]);
}
#[test]
fn a_box_that_becomes_a_float_stays_under_the_float_it_held() {
let mut core = Core::new();
let build = |core: &mut Core, floating: bool| {
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
let card = if floating {
float_at(100.0, RED)
} else {
NodeSpec::column().size(200.0, 100.0).bg(RED)
};
ui.with_keyed("card", card, |ui| {
ui.leaf_keyed("name", float_at(120.0, BLUE));
ui.leaf_keyed("photo", NodeSpec::row().size(40.0, 20.0).bg(GREEN));
});
ui.finish();
};
build(&mut core, false);
assert_eq!(painted(&mut core), ["red", "green", "blue"]);
build(&mut core, true);
assert_eq!(
painted(&mut core),
["red", "green", "blue"],
"the name panel is above the card that became a float"
);
build(&mut core, true);
assert_eq!(
painted(&mut core),
["red", "green", "blue"],
"and stays there"
);
}
#[test]
fn two_floats_held_by_a_box_that_becomes_a_float_keep_their_order() {
let mut core = Core::new();
let build = |core: &mut Core, floating: bool| {
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
let card = if floating {
float_at(100.0, RED)
} else {
NodeSpec::column().size(200.0, 100.0).bg(RED)
};
ui.with_keyed("card", card, |ui| {
ui.leaf_keyed("name", float_at(120.0, BLUE));
ui.leaf_keyed("tip", float_at(130.0, GREEN));
});
ui.finish();
};
build(&mut core, false);
assert_eq!(painted(&mut core), ["red", "blue", "green"]);
build(&mut core, true);
assert_eq!(
painted(&mut core),
["red", "blue", "green"],
"the tip, opened over the name, stays over it"
);
build(&mut core, true);
assert_eq!(painted(&mut core), ["red", "blue", "green"]);
}
#[test]
fn a_float_moved_into_a_float_under_its_own_key_goes_above_it() {
let mut core = Core::new();
let tip = kui_core::Key::ROOT.str("tip");
let build = |core: &mut Core, panel: bool, inside: bool| {
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
if panel {
ui.with_keyed("panel", float_at(100.0, RED), |ui| {
if inside {
ui.leaf_key(tip, float_at(120.0, BLUE));
}
});
}
if !inside {
ui.leaf_key(tip, float_at(120.0, BLUE));
}
ui.finish();
};
build(&mut core, false, false);
assert_eq!(painted(&mut core), ["blue"]);
build(&mut core, true, false);
assert_eq!(
painted(&mut core),
["blue", "red"],
"the panel opened over the tip"
);
build(&mut core, true, true);
assert_eq!(
painted(&mut core),
["red", "blue"],
"the tip is above the panel it moved into"
);
build(&mut core, true, true);
assert_eq!(painted(&mut core), ["red", "blue"]);
}
#[test]
fn a_float_over_a_modal_from_outside_it_warns_when_it_holds_a_control() {
let mut core = Core::new();
let build = |core: &mut Core, hud: bool, button: bool| {
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.leaf_keyed(
"dialog",
float_at(100.0, RED).modal("dialog").label("Dialog"),
);
if hud {
ui.with_keyed("hud", float_at(150.0, BLUE), |ui| {
if button {
ui.leaf_keyed(
"close",
NodeSpec::row()
.size(20.0, 20.0)
.on_click("close")
.label("Close"),
);
}
});
}
ui.finish();
};
build(&mut core, false, false);
assert!(core.take_warnings().is_empty());
build(&mut core, true, false);
assert!(core.take_warnings().is_empty(), "a picture over the dialog");
build(&mut core, true, true);
let codes: Vec<_> = core.take_warnings().iter().map(|w| w.code).collect();
assert_eq!(codes, ["modal-behind-content"], "a control over it");
}
fn painted_with_strokes(core: &mut Core) -> Vec<&'static str> {
let (dl, _) = core.output();
dl.quads
.iter()
.filter(|q| matches!(q.kind, QuadKind::Solid | QuadKind::Segment))
.map(|q| {
if q.kind == QuadKind::Segment {
"stroke"
} else if q.color == RED {
"red"
} else if q.color == BLUE {
"blue"
} else if q.color == GREY {
"grey"
} else {
"?"
}
})
.collect()
}
fn toast_and_bar(core: &mut Core, glyph: bool, viewport: bool) {
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed(
"bar",
NodeSpec::row().grow_width().height(24.0).bg(GREY),
|ui| {
if glyph {
let mut spec = NodeSpec::row();
if viewport {
spec = spec.float(FloatConfig::viewport());
}
ui.polyline_keyed(
"glyph",
&[Vec2::new(300.0, 4.0), Vec2::new(316.0, 20.0)],
kui_core::Stroke::new(2.0, GREEN),
spec,
);
}
},
);
ui.leaf_keyed("toast", float_at(200.0, RED));
ui.finish();
}
#[test]
fn a_stroke_paints_in_its_parents_layer_not_in_one_of_its_own() {
let mut core = Core::new();
toast_and_bar(&mut core, false, false);
assert_eq!(painted_with_strokes(&mut core), ["grey", "red"]);
toast_and_bar(&mut core, true, false);
assert_eq!(
painted_with_strokes(&mut core),
["grey", "stroke", "red"],
"the glyph came a frame after the toast and is still under it"
);
toast_and_bar(&mut core, true, false);
assert_eq!(painted_with_strokes(&mut core), ["grey", "stroke", "red"]);
let mut core = Core::new();
toast_and_bar(&mut core, false, true);
toast_and_bar(&mut core, true, true);
assert_eq!(
painted_with_strokes(&mut core),
["grey", "red", "stroke"],
"a viewport-anchored stroke is a layer, opened after the toast's"
);
}
#[test]
fn a_stroke_inside_a_float_paints_in_that_floats_layer() {
fn frame(core: &mut Core, stroke: bool) {
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed("first", float_at(100.0, RED), |ui| {
if stroke {
ui.line(
Vec2::new(10.0, 10.0),
Vec2::new(190.0, 90.0),
kui_core::Stroke::new(2.0, GREEN),
NodeSpec::row(),
);
}
});
ui.leaf_keyed("second", float_at(150.0, BLUE));
ui.finish();
}
let mut core = Core::new();
frame(&mut core, false);
assert_eq!(painted_with_strokes(&mut core), ["red", "blue"]);
frame(&mut core, true);
assert_eq!(painted_with_strokes(&mut core), ["red", "stroke", "blue"]);
}
fn painted_shapes(core: &mut Core) -> Vec<&'static str> {
let (dl, _) = core.output();
dl.quads
.iter()
.filter_map(|q| match q.kind {
QuadKind::Solid => Some(if q.color == RED {
"red"
} else if q.color == BLUE {
"blue"
} else if q.color == GREY {
"grey"
} else {
"?"
}),
QuadKind::Segment => Some("stroke"),
QuadKind::Fragment => Some("polygon"),
QuadKind::GlyphMask => Some("path"),
_ => None,
})
.collect()
}
#[test]
fn a_polygon_and_a_path_paint_in_their_parents_layer_as_a_line_does() {
fn frame(core: &mut Core, shapes: bool) {
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed(
"bar",
NodeSpec::row().grow_width().height(24.0).bg(GREY),
|ui| {
if shapes {
ui.polygon_keyed(
"wedge",
&[
Vec2::new(300.0, 4.0),
Vec2::new(316.0, 4.0),
Vec2::new(308.0, 20.0),
],
NodeSpec::row().bg(GREEN),
);
ui.path_d_keyed(
"icon",
"M 330 4 L 346 4 L 338 20 Z",
kui_core::FillRule::NonZero,
None,
None,
NodeSpec::row().bg(GREEN),
);
}
},
);
ui.leaf_keyed("toast", float_at(200.0, RED));
ui.finish();
}
let mut core = Core::new();
frame(&mut core, false);
assert_eq!(painted_shapes(&mut core), ["grey", "red"]);
frame(&mut core, true);
assert_eq!(
painted_shapes(&mut core),
["grey", "polygon", "path", "red"],
"both came a frame after the toast and are under it"
);
}
#[test]
fn a_departing_stroke_is_replayed_in_its_parents_layer() {
fn frame(core: &mut Core, now: f64, glyph: bool) {
core.set_time(now);
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed(
"bar",
NodeSpec::row().grow_width().height(24.0).bg(GREY),
|ui| {
if glyph {
ui.line_keyed(
"glyph",
Vec2::new(300.0, 4.0),
Vec2::new(316.0, 20.0),
kui_core::Stroke::new(2.0, GREEN),
NodeSpec::row()
.transition(100.0)
.exit(kui_core::Enter::default().opacity(0.0)),
);
}
},
);
ui.leaf_keyed("toast", float_at(200.0, RED));
ui.finish();
}
let mut core = Core::new();
frame(&mut core, 0.0, false);
frame(&mut core, 0.0, true);
assert_eq!(painted_with_strokes(&mut core), ["grey", "stroke", "red"]);
frame(&mut core, 0.01, false);
assert!(core.animating(), "the exit is playing");
assert_eq!(
painted_with_strokes(&mut core),
["grey", "stroke", "red"],
"the ghost keeps the stroke's place under the toast"
);
frame(&mut core, 0.2, false);
assert_eq!(painted_with_strokes(&mut core), ["grey", "red"]);
}
#[test]
fn a_declared_float_with_clip_is_still_a_layer_of_its_own() {
let mut core = Core::new();
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed("canvas", NodeSpec::column().fill().clip().bg(GREY), |ui| {
ui.leaf_keyed(
"node",
NodeSpec::column()
.size(80.0, 40.0)
.bg(RED)
.float(FloatConfig::parent().clipped().offset(10.0, 10.0)),
);
ui.leaf_keyed("sheet", NodeSpec::column().size(200.0, 200.0).bg(BLUE));
});
ui.finish();
assert_eq!(
painted(&mut core),
["grey", "blue", "red"],
"the declared float paints after the sibling declared after it"
);
}
#[test]
fn a_viewport_anchored_stroke_still_escapes_its_parents_clip() {
let mut core = Core::new();
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with(NodeSpec::column().size(50.0, 50.0).clip(), |ui| {
ui.line(
Vec2::new(0.0, 25.0),
Vec2::new(300.0, 25.0),
kui_core::Stroke::new(2.0, GREEN),
NodeSpec::row(),
);
ui.line(
Vec2::new(0.0, 150.0),
Vec2::new(300.0, 150.0),
kui_core::Stroke::new(2.0, GREEN),
NodeSpec::row().float(FloatConfig::viewport()),
);
});
ui.finish();
let (dl, _) = core.output();
let clips: Vec<kui_core::Rect> = dl
.quads
.iter()
.filter(|q| q.kind == QuadKind::Segment)
.map(|q| dl.clips[q.clip as usize].rect)
.collect();
assert_eq!(clips.len(), 2, "both strokes draw");
assert_eq!(
(clips[0].w, clips[0].h),
(50.0, 50.0),
"held by the box's clip: {:?}",
clips[0]
);
assert!(
clips[1].contains(Vec2::new(150.0, 150.0)),
"escaped the box: {:?}",
clips[1]
);
}
#[test]
fn a_stroke_is_hit_at_its_place_among_its_siblings() {
fn frame(core: &mut Core, stroke_first: bool) {
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed(
"bar",
NodeSpec::row().grow_width().height(24.0).bg(GREY),
|ui| {
let glyph = |ui: &mut kui_core::Ui<'_>| {
ui.line_keyed(
"glyph",
Vec2::new(0.0, 12.0),
Vec2::new(100.0, 12.0),
kui_core::Stroke::new(4.0, GREEN),
NodeSpec::row().on_click("glyph"),
);
};
let card = |ui: &mut kui_core::Ui<'_>| {
ui.leaf_keyed(
"card",
NodeSpec::column()
.size(60.0, 24.0)
.bg(BLUE)
.on_click("card"),
);
};
if stroke_first {
glyph(ui);
card(ui);
} else {
card(ui);
glyph(ui);
}
},
);
ui.finish();
}
fn click_at(core: &mut Core, x: f32, y: f32) -> Option<String> {
core.handle_input(InputEvent::CursorMoved(Vec2::new(x, y)));
core.handle_input(InputEvent::mouse_down(1));
let evs = core.handle_input(InputEvent::mouse_up());
evs.first()
.and_then(|e| e.payload.as_str())
.map(str::to_string)
}
let mut core = Core::new();
frame(&mut core, true);
assert_eq!(painted_with_strokes(&mut core), ["grey", "stroke", "blue"]);
assert_eq!(
click_at(&mut core, 30.0, 12.0).as_deref(),
Some("card"),
"the card declared after the stroke is over it"
);
assert_eq!(click_at(&mut core, 80.0, 12.0).as_deref(), Some("glyph"));
let mut core = Core::new();
frame(&mut core, false);
assert_eq!(painted_with_strokes(&mut core), ["grey", "blue", "stroke"]);
assert_eq!(
click_at(&mut core, 30.0, 12.0).as_deref(),
Some("glyph"),
"declared after the card, the stroke is over it"
);
}
#[test]
fn an_images_border_is_drawn_over_the_picture() {
let mut core = Core::new();
let img = core.resources.add_image(2, 2, vec![0xff; 16]);
let mut ui = core.frame(VIEW, 1.0);
ui.configure_root(NodeSpec::column().fill());
ui.image(
img,
NodeSpec::column()
.size(80.0, 80.0)
.bg(GREY)
.radius(40.0)
.border(3.0, RED),
);
ui.finish();
let (dl, _) = core.output();
let kinds: Vec<(QuadKind, bool)> = dl
.quads
.iter()
.map(|q| (q.kind, q.border_w > 0.0 && q.border_color == RED))
.collect();
let image = kinds.iter().position(|(k, _)| *k == QuadKind::Image);
let ring = kinds.iter().position(|(_, ringed)| *ringed);
assert!(
matches!((image, ring), (Some(i), Some(r)) if r > i),
"the border after the picture: {kinds:?}"
);
assert_eq!(
kinds.iter().filter(|(_, ringed)| *ringed).count(),
1,
"drawn once: {kinds:?}"
);
let bg = dl.quads.iter().position(|q| q.color == GREY).unwrap();
assert!(bg < image.unwrap(), "the background stays under it");
}