use kui_core::diag::{CONTROL_WITHOUT_NAME, IMAGE_WITHOUT_LABEL, SLIDER_VALUE_OUT_OF_RANGE};
use kui_core::testing::codes;
use kui_core::{
AccessAction, AccessRequest, AccessTree, Core, EditOptions, InputEvent, Key, NodeSpec, Role,
Size, TextStyle, UiEvent, Value, WindowButton, WindowCommand,
};
fn kinds(evs: &[UiEvent]) -> Vec<String> {
evs.iter()
.map(|ev| match ev.kind() {
Some(k) => k.to_string(),
None => format!("{:?}", ev.payload),
})
.collect()
}
fn names(tree: &AccessTree) -> Vec<(Role, Option<&str>)> {
tree.nodes
.iter()
.map(|n| (n.role, n.name.as_deref()))
.collect()
}
fn app_frame(core: &mut Core, focus_edit: bool) -> (Key, Key, Key) {
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.window_title("Demo");
ui.configure_root(NodeSpec::column().fill());
ui.with_keyed("titlebar", NodeSpec::row().window_drag(), |ui| {
ui.text_in_keyed(
"close",
NodeSpec::row().window_button(WindowButton::Close),
"×",
TextStyle::new(12.0),
);
});
ui.leaf_keyed("icon", NodeSpec::row().on_click("save").label("Save"));
let mut button = Key::ROOT;
ui.with_keyed("wrap", NodeSpec::column().pad(4.0), |ui| {
ui.with(NodeSpec::column(), |ui| {
button = ui.with_keyed("go", NodeSpec::row().on_click("go"), |ui| {
ui.text("Go", TextStyle::new(14.0));
ui.text("now", TextStyle::new(14.0));
});
});
});
let id = core_image(ui.core());
ui.image(id, NodeSpec::column().label("Logo"));
ui.image(id, NodeSpec::column().role(Role::None));
let edit = ui.text_edit(
"name",
"hello",
&EditOptions {
autofocus: focus_edit,
..Default::default()
},
NodeSpec::column().width(200.0).label("Name"),
);
let list = ui.with_keyed(
"list",
NodeSpec::column().height(100.0).scroll_y().role(Role::List),
|ui| {
for i in 0..10 {
ui.text_in_keyed(
&format!("row-{i}"),
NodeSpec::row().height(30.0).role(Role::ListItem),
&format!("Row {i}"),
TextStyle::new(12.0),
);
}
},
);
ui.finish();
(button, edit, list)
}
fn core_image(core: &mut Core) -> kui_core::ImageId {
core.resources.add_image(4, 4, vec![255; 64])
}
#[test]
fn roles_derive_from_behaviour_and_plain_boxes_are_elided() {
let mut core = Core::new();
let (button, edit, list) = app_frame(&mut core, false);
let tree = core.access_tree().clone();
let root = tree.root().unwrap();
assert_eq!(root.role, Role::Window);
assert_eq!(root.name.as_deref(), Some("Demo"));
assert_eq!(root.rect, kui_core::Rect::new(0.0, 0.0, 400.0, 300.0));
let got = names(&tree);
assert_eq!(got[1], (Role::TitleBar, None));
assert_eq!(
tree.nodes
.iter()
.filter(|n| n.name.as_deref() == Some("Demo"))
.count(),
1,
"the window title is announced once"
);
assert_eq!(got[2], (Role::Button, Some("Close")));
assert_eq!(got[3], (Role::Button, Some("Save")));
assert_eq!(got[4], (Role::Button, Some("Go now")));
let go = tree.get(button).unwrap();
assert_eq!(go.parent, Some(Key::ROOT));
assert!(go.supports(AccessAction::Click));
assert!(!go.supports(AccessAction::SetValue));
assert_eq!(got[5], (Role::Image, Some("Logo")));
assert_eq!(
tree.nodes.iter().filter(|n| n.role == Role::Image).count(),
1
);
let field = tree.get(edit).unwrap();
assert_eq!(field.role, Role::TextInput);
assert_eq!(field.name.as_deref(), Some("Name"));
assert_eq!(field.value.as_deref(), Some("hello"));
assert_eq!(
field.caret,
Some(5),
"a fresh single-line editor's caret sits after its seeded text (F20)"
);
assert_eq!(field.selection, None);
assert!(!field.focused);
assert!(field.supports(AccessAction::SetValue));
assert!(field.supports(AccessAction::Focus));
let rows = tree.get(list).unwrap();
assert_eq!(rows.role, Role::List);
let scroll = rows.scroll.unwrap();
assert_eq!((scroll.x, scroll.y), (0.0, 0.0));
assert_eq!(scroll.max_y, 200.0);
assert!(rows.supports(AccessAction::ScrollDown));
let items: Vec<_> = tree.children(list).collect();
assert_eq!(items.len(), 10);
assert_eq!(items[0].role, Role::ListItem);
assert_eq!(items[0].name, None, "a list item is not named by content");
let texts: Vec<_> = tree.children(items[3].key).collect();
assert_eq!(texts.len(), 1);
assert_eq!(texts[0].role, Role::StaticText);
assert_eq!(texts[0].name.as_deref(), Some("Row 3"));
assert_eq!(tree.focus, None);
assert_eq!(tree.nodes.len(), 28);
}
#[test]
fn the_tree_is_built_once_per_frame_and_hashes_its_content() {
let mut core = Core::new();
app_frame(&mut core, false);
let first = core.access_tree().hash;
assert_eq!(core.access_tree().hash, first, "same frame, same tree");
app_frame(&mut core, false);
assert_eq!(
core.access_tree().hash,
first,
"identical frames hash alike"
);
app_frame(&mut core, true);
assert_ne!(core.access_tree().hash, first, "focus is part of the tree");
}
#[test]
fn explicit_roles_win_and_carry_their_state() {
let mut core = Core::new();
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
let check = ui.text_in_keyed(
"check",
NodeSpec::row()
.role(Role::Checkbox)
.checked(true)
.on_click("toggle"),
"Remember me",
TextStyle::new(12.0),
);
let slider = ui.leaf_keyed(
"vol",
NodeSpec::row()
.role(Role::Slider)
.label("Volume")
.value_now(0.4)
.value_min(0.0)
.value_max(1.0)
.on_drag("vol"),
);
let text_slider = ui.leaf_keyed(
"focus",
NodeSpec::row()
.role(Role::Slider)
.label("Focus length")
.value_now(25.0)
.value_min(5.0)
.value_max(60.0)
.value_text("25 minutes"),
);
let heading = ui.text_in_keyed(
"h",
NodeSpec::row().role(Role::Heading),
"Settings",
TextStyle::new(20.0),
);
let tab = ui.text_in_keyed(
"tab",
NodeSpec::row().role(Role::Tab).on_click(Value::Int(1)),
"General",
TextStyle::new(12.0),
);
ui.finish();
let tree = core.access_tree().clone();
let c = tree.get(check).unwrap();
assert_eq!(c.role, Role::Checkbox);
assert_eq!(c.checked, Some(true));
assert_eq!(c.name.as_deref(), Some("Remember me"));
assert!(c.supports(AccessAction::Click));
let s = tree.get(slider).unwrap();
assert_eq!(s.role, Role::Slider);
assert_eq!((s.number, s.min, s.max), (Some(0.4), Some(0.0), Some(1.0)));
assert!(s.supports(AccessAction::Increment));
assert!(!s.supports(AccessAction::Click), "no click payload");
assert_eq!(
s.value, None,
"a slider that named no reading has no string value"
);
let t = tree.get(text_slider).unwrap();
assert_eq!(
t.value.as_deref(),
Some("25 minutes"),
"value_text lands in the node's one string slot, which is what the \
platform reads instead of the number (backlog F8)"
);
assert_eq!(
(t.number, t.min, t.max),
(Some(25.0), Some(5.0), Some(60.0)),
"the range still travels: only the reading is replaced"
);
assert!(t.supports(AccessAction::Increment), "still nudgeable");
let h = tree.get(heading).unwrap();
assert_eq!(h.role, Role::Heading);
assert_eq!(h.name.as_deref(), Some("Settings"));
assert!(
tree.children(heading).next().is_none(),
"a heading's text is its name, not a child"
);
assert_eq!(tree.get(tab).unwrap().role, Role::Tab);
assert_eq!(tree.get(tab).unwrap().name.as_deref(), Some("General"));
}
#[test]
fn selected_marks_the_current_one_of_a_set() {
let mut core = Core::new();
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
let tabs = ui.with_keyed("tabs", NodeSpec::row().role(Role::TabList), |ui| {
for (i, name) in ["General", "Network", "About"].iter().enumerate() {
ui.text_in_keyed(
name,
NodeSpec::row()
.role(Role::Tab)
.selected(i == 1)
.on_click(Value::Int(i as i64)),
name,
TextStyle::new(12.0),
);
}
});
let list = ui.with_keyed("rows", NodeSpec::column().role(Role::List), |ui| {
for (i, name) in ["one", "two"].iter().enumerate() {
ui.text_in_keyed(
name,
NodeSpec::row().role(Role::ListItem).selected(i == 0),
name,
TextStyle::new(12.0),
);
}
});
let away = ui.text_in_keyed(
"away",
NodeSpec::row().role(Role::Link),
"Docs",
TextStyle::new(12.0),
);
let here = ui.text_in_keyed(
"here",
NodeSpec::row().role(Role::Link).selected(true),
"Home",
TextStyle::new(12.0),
);
ui.finish();
let tree = core.access_tree().clone();
let states: Vec<Option<bool>> = tree.children(tabs).map(|n| n.selected).collect();
assert_eq!(
states,
vec![Some(false), Some(true), Some(false)],
"every tab reports the state, so a reader can say which one is on"
);
let rows: Vec<Option<bool>> = tree.children(list).map(|n| n.selected).collect();
assert_eq!(
rows,
vec![Some(true), None],
"a row says so only when picked: an ordinary list is not a selection"
);
assert_eq!(tree.get(away).unwrap().selected, None);
assert_eq!(tree.get(here).unwrap().selected, Some(true));
assert_eq!(
tree.get(tabs).unwrap().selected,
None,
"the container itself is not selectable"
);
assert!(tree.children(tabs).all(|n| n.checked.is_none()));
}
#[test]
fn expanded_is_three_state_so_a_shut_disclosure_can_say_so() {
let mut core = Core::new();
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
let shut = ui.text_in_keyed(
"shut",
NodeSpec::row().expanded(false).on_click("toggle"),
"Advanced",
TextStyle::new(12.0),
);
let open = ui.text_in_keyed(
"open",
NodeSpec::row().expanded(true).on_click("toggle"),
"Network",
TextStyle::new(12.0),
);
let plain = ui.text_in_keyed(
"plain",
NodeSpec::row().on_click("go"),
"Save",
TextStyle::new(12.0),
);
ui.finish();
let tree = core.access_tree().clone();
assert_eq!(tree.get(shut).unwrap().expanded, Some(false));
assert_eq!(tree.get(open).unwrap().expanded, Some(true));
assert_eq!(
tree.get(plain).unwrap().expanded,
None,
"an ordinary button does not expand, and says nothing about it"
);
}
#[test]
fn a_list_numbers_its_own_items() {
let mut core = Core::new();
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
let list = ui.with_keyed("rows", NodeSpec::column().role(Role::List), |ui| {
for name in ["one", "two", "three"] {
ui.text_in_keyed(
name,
NodeSpec::row().role(Role::ListItem),
name,
TextStyle::new(12.0),
);
}
ui.text_in_keyed(
"caption",
NodeSpec::row().role(Role::Heading),
"3 rows",
TextStyle::new(12.0),
);
});
let loose = ui.text_in_keyed(
"loose",
NodeSpec::row().role(Role::ListItem),
"orphan",
TextStyle::new(12.0),
);
ui.finish();
let tree = core.access_tree().clone();
assert_eq!(
tree.get(list).unwrap().set_size,
Some(3),
"the count sits on the container, the way AccessKit models a set"
);
assert_eq!(tree.get(list).unwrap().pos_in_set, None);
let ordinals: Vec<Option<usize>> = tree
.children(list)
.filter(|n| n.role == Role::ListItem)
.map(|n| n.pos_in_set)
.collect();
assert_eq!(ordinals, vec![Some(0), Some(1), Some(2)], "zero-based");
assert!(
tree.children(list)
.all(|n| n.role == Role::ListItem || n.pos_in_set.is_none()),
"a heading among the rows is not one of them"
);
assert_eq!(
tree.get(loose).unwrap().pos_in_set,
None,
"an item outside a list belongs to no set"
);
}
#[test]
fn a_click_request_emits_what_a_pointer_click_would() {
let mut core = Core::new();
let (button, _, _) = app_frame(&mut core, false);
let evs = core.handle_input(InputEvent::Access(AccessRequest {
key: button,
action: AccessAction::Click,
value: None,
anchor: None,
focus: None,
}));
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].key, button);
assert_eq!(evs[0].payload.as_str(), Some("go"));
let close = Key::ROOT.str("titlebar").str("close");
let evs = core.handle_input(InputEvent::Access(AccessRequest {
key: close,
action: AccessAction::Click,
value: None,
anchor: None,
focus: None,
}));
assert!(evs.is_empty());
assert_eq!(
core.take_window_commands(),
vec![WindowCommand::Close(kui_core::WindowId::MAIN)]
);
let evs = core.handle_input(InputEvent::Access(AccessRequest {
key: Key::ROOT.str("nope"),
action: AccessAction::Click,
value: None,
anchor: None,
focus: None,
}));
assert!(evs.is_empty());
}
#[test]
fn focus_and_value_requests_drive_an_editor() {
let mut core = Core::new();
let (_, edit, _) = app_frame(&mut core, false);
assert!(!core.is_focused(edit));
let evs = core.handle_input(InputEvent::Access(AccessRequest {
key: edit,
action: AccessAction::Focus,
value: None,
anchor: None,
focus: None,
}));
assert!(evs.is_empty());
assert!(core.is_focused(edit));
app_frame(&mut core, false);
let tree = core.access_tree().clone();
assert_eq!(tree.focus, Some(edit));
assert!(tree.get(edit).unwrap().focused);
let evs = core.handle_input(InputEvent::Access(AccessRequest {
key: edit,
action: AccessAction::SetValue,
value: Some("world".into()),
anchor: None,
focus: None,
}));
assert_eq!(kinds(&evs), ["changed"]);
assert_eq!(evs[0].key, edit);
assert_eq!(core.edit_text(edit).as_deref(), Some("world"));
let evs = core.handle_input(InputEvent::Access(AccessRequest {
key: edit,
action: AccessAction::SetValue,
value: Some("world".into()),
anchor: None,
focus: None,
}));
assert!(evs.is_empty());
core.handle_input(InputEvent::Access(AccessRequest {
key: edit,
action: AccessAction::Blur,
value: None,
anchor: None,
focus: None,
}));
assert!(!core.is_focused(edit));
core.handle_input(InputEvent::Access(AccessRequest {
key: edit,
action: AccessAction::Click,
value: None,
anchor: None,
focus: None,
}));
assert!(core.is_focused(edit));
}
#[test]
fn slider_nudges_reach_the_app_as_access_events() {
let mut core = Core::new();
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
let slider = ui.leaf_keyed(
"vol",
NodeSpec::row()
.role(Role::Slider)
.label("Volume")
.on_drag("vol"),
);
let untagged = ui.leaf_keyed("bare", NodeSpec::row().role(Role::Slider).label("x"));
ui.finish();
let evs = core.handle_input(InputEvent::Access(AccessRequest {
key: slider,
action: AccessAction::Increment,
value: None,
anchor: None,
focus: None,
}));
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].key, slider);
assert_eq!(
evs[0].payload,
Value::map([
("kind", Value::str("access")),
("action", Value::str("increment")),
("tag", Value::str("vol")),
])
);
let evs = core.handle_input(InputEvent::Access(AccessRequest {
key: untagged,
action: AccessAction::Decrement,
value: None,
anchor: None,
focus: None,
}));
assert_eq!(
evs[0].payload,
Value::map([
("kind", Value::str("access")),
("action", Value::str("decrement")),
])
);
}
#[test]
fn scroll_requests_move_the_scroll_view() {
let mut core = Core::new();
let (_, _, list) = app_frame(&mut core, false);
let row8 = list.str("row-8");
let before = core.access_tree().get(row8).unwrap().rect;
assert_eq!(
(before.w, before.h),
(0.0, 0.0),
"row 8 starts out of view: {before:?}"
);
core.handle_input(InputEvent::Access(AccessRequest {
key: row8,
action: AccessAction::ScrollIntoView,
value: None,
anchor: None,
focus: None,
}));
app_frame(&mut core, false);
let tree = core.access_tree().clone();
let view = tree.get(list).unwrap();
let row = tree.get(row8).unwrap();
assert!(row.rect.y >= view.rect.y && row.rect.y + row.rect.h <= view.rect.y + view.rect.h);
assert!(view.scroll.unwrap().y > 0.0);
core.handle_input(InputEvent::Access(AccessRequest {
key: list,
action: AccessAction::ScrollUp,
value: None,
anchor: None,
focus: None,
}));
app_frame(&mut core, false);
let after = core.access_tree().get(list).unwrap().scroll.unwrap().y;
assert!(after < view.scroll.unwrap().y);
}
#[test]
fn requests_behind_a_modal_or_on_a_disabled_node_do_nothing() {
let mut core = Core::new();
let build = |core: &mut Core, dialog: bool| {
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
let field = ui.text_edit(
"name",
"before",
&EditOptions::default(),
NodeSpec::row().size(100.0, 20.0),
);
let slider = ui.leaf_keyed(
"vol",
NodeSpec::row()
.size(100.0, 20.0)
.role(Role::Slider)
.label("Volume")
.on_drag("vol"),
);
let off = ui.leaf_keyed(
"off",
NodeSpec::row()
.size(100.0, 20.0)
.role(Role::Slider)
.label("Muted")
.disabled(true)
.on_drag("off"),
);
let list = ui.with_keyed(
"list",
NodeSpec::column()
.size(100.0, 40.0)
.scroll_y()
.label("List"),
|ui| {
for i in 0..10 {
ui.leaf_keyed(&format!("row-{i}"), NodeSpec::row().size(100.0, 20.0));
}
},
);
if dialog {
ui.with_keyed(
"dialog",
NodeSpec::column()
.size(120.0, 80.0)
.modal("dlg")
.label("Settings"),
|ui| {
ui.leaf_keyed(
"ok",
NodeSpec::row().size(80.0, 20.0).on_click("ok").label("OK"),
);
},
);
}
ui.finish();
(field, slider, off, list)
};
let (field, slider, off, list) = build(&mut core, true);
let req = |key: Key, action: AccessAction| InputEvent::Access(AccessRequest::new(key, action));
let evs = core.handle_input(InputEvent::Access(
AccessRequest::new(field, AccessAction::SetValue).with_value("after"),
));
assert!(evs.is_empty(), "{evs:?}");
assert_eq!(core.edit_text(field).as_deref(), Some("before"));
assert!(
core.handle_input(req(slider, AccessAction::Increment))
.is_empty()
);
core.handle_input(req(field, AccessAction::Focus));
assert_ne!(core.focus(), Some(field), "focus stays inside the modal");
core.handle_input(req(list, AccessAction::ScrollDown));
build(&mut core, true);
assert_eq!(
core.access_tree().get(list).unwrap().scroll.unwrap().y,
0.0,
"the list behind the dialog did not scroll"
);
build(&mut core, false);
let evs = core.handle_input(InputEvent::Access(
AccessRequest::new(field, AccessAction::SetValue).with_value("after"),
));
assert_eq!(kinds(&evs), ["changed"]);
assert_eq!(core.edit_text(field).as_deref(), Some("after"));
assert_eq!(
kinds(&core.handle_input(req(slider, AccessAction::Increment))),
["access"]
);
assert!(
core.handle_input(req(off, AccessAction::Increment))
.is_empty(),
"disabled"
);
core.handle_input(req(list, AccessAction::ScrollDown));
build(&mut core, false);
assert!(core.access_tree().get(list).unwrap().scroll.unwrap().y > 0.0);
}
#[test]
fn a_click_behind_a_modal_fires_nothing_and_asks_the_modal_to_go() {
let mut core = Core::new();
let build = |core: &mut Core| {
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
let close = ui.leaf_keyed(
"close",
NodeSpec::row()
.size(20.0, 20.0)
.window_button(WindowButton::Close)
.label("Close"),
);
let save = ui.leaf_keyed(
"save",
NodeSpec::row()
.size(100.0, 20.0)
.on_click(Value::map([("kind", Value::str("save"))]))
.label("Save"),
);
let off = ui.leaf_keyed(
"off",
NodeSpec::row()
.size(100.0, 20.0)
.on_click(Value::map([("kind", Value::str("off"))]))
.disabled(true)
.label("Off"),
);
let dialog = ui.with_keyed(
"dialog",
NodeSpec::column()
.size(120.0, 80.0)
.modal("dlg")
.label("Settings"),
|ui| {
ui.leaf_keyed(
"ok",
NodeSpec::row()
.size(80.0, 20.0)
.on_click(Value::map([("kind", Value::str("ok"))]))
.label("OK"),
);
},
);
let ok = ui.core().key_of("ok").unwrap();
ui.finish();
(close, save, off, dialog, ok)
};
let (close, save, off, dialog, ok) = build(&mut core);
assert_eq!(core.focus(), Some(ok), "focus went into the modal");
let click = |core: &mut Core, key: Key| {
core.handle_input(InputEvent::Access(AccessRequest::new(
key,
AccessAction::Click,
)))
};
let evs = click(&mut core, save);
assert_eq!(kinds(&evs), ["dismiss"]);
assert_eq!(evs[0].key, dialog);
assert_eq!(evs[0].payload.get_str("reason"), Some("outside"));
assert_eq!(evs[0].payload.get_str("tag"), Some("dlg"));
assert_eq!(core.focus(), Some(ok));
assert_eq!(kinds(&click(&mut core, off)), ["dismiss"]);
assert_eq!(kinds(&click(&mut core, ok)), ["ok"]);
assert!(click(&mut core, close).is_empty());
assert_eq!(
core.take_window_commands(),
[WindowCommand::Close(kui_core::WindowId::MAIN)]
);
assert!(click(&mut core, dialog).is_empty());
let mut plain = Core::new();
let mut ui = plain.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
let save = ui.leaf_keyed(
"save",
NodeSpec::row()
.size(100.0, 20.0)
.on_click(Value::map([("kind", Value::str("save"))]))
.label("Save"),
);
ui.finish();
assert_eq!(kinds(&click(&mut plain, save)), ["save"]);
}
#[test]
fn missing_names_are_warnings_raised_once() {
let mut core = Core::new();
let frame = |core: &mut Core| {
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
let id = core_image(ui.core());
ui.image(id, NodeSpec::column());
ui.image(id, NodeSpec::column().label("fine"));
ui.image(id, NodeSpec::column().role(Role::None));
ui.leaf_keyed("icon", NodeSpec::row().on_click(Value::Int(1)));
ui.text_in_keyed(
"ok",
NodeSpec::row().on_click(Value::Int(2)),
"OK",
TextStyle::new(12.0),
);
ui.leaf(NodeSpec::row());
ui.text_edit("q", "", &EditOptions::default(), NodeSpec::column());
ui.finish();
};
frame(&mut core);
let ws = core.take_warnings();
assert_eq!(
codes(&ws),
[
IMAGE_WITHOUT_LABEL,
CONTROL_WITHOUT_NAME,
CONTROL_WITHOUT_NAME
]
);
assert_eq!(ws[1].key, Key::ROOT.str("icon"));
assert!(ws[1].message.contains("button"), "{}", ws[1].message);
assert!(ws[2].message.contains("textInput"), "{}", ws[2].message);
frame(&mut core);
assert!(core.take_warnings().is_empty(), "each once");
}
#[test]
fn slider_value_outside_its_range_warns() {
let mut core = Core::new();
let slider = |label: &'static str| NodeSpec::row().role(Role::Slider).label(label);
let frame = |core: &mut Core| {
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.leaf_keyed(
"fine",
slider("fine").value_now(5.0).value_min(0.0).value_max(10.0),
);
ui.leaf_keyed(
"below",
slider("below")
.value_now(-1.0)
.value_min(0.0)
.value_max(10.0),
);
ui.leaf_keyed(
"above",
slider("above")
.value_now(999.0)
.value_min(0.0)
.value_max(10.0),
);
ui.leaf_keyed(
"inverted",
slider("inverted")
.value_now(5.0)
.value_min(10.0)
.value_max(0.0),
);
ui.leaf_keyed("open", slider("open").value_now(999.0));
ui.finish();
};
frame(&mut core);
let ws = core.take_warnings();
assert_eq!(
codes(&ws),
[
SLIDER_VALUE_OUT_OF_RANGE,
SLIDER_VALUE_OUT_OF_RANGE,
SLIDER_VALUE_OUT_OF_RANGE
]
);
assert_eq!(ws[0].key, Key::ROOT.str("below"));
assert!(
ws[0].message.contains("below valueMin 0"),
"{}",
ws[0].message
);
assert_eq!(ws[1].key, Key::ROOT.str("above"));
assert!(
ws[1].message.contains("above valueMax 10"),
"{}",
ws[1].message
);
assert_eq!(ws[2].key, Key::ROOT.str("inverted"));
assert!(
ws[2].message.contains("valueMin 10 is above valueMax 0"),
"{}",
ws[2].message
);
frame(&mut core);
assert!(core.take_warnings().is_empty(), "each once");
}
#[test]
fn a_hovered_control_keeps_its_name_and_its_hint_is_the_description() {
use kui_core::schema::PropsOut;
use kui_core::{Content, QuadKind, Vec2, widgets};
let mut core = Core::new();
let frame = |core: &mut Core| {
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().gap(60.0));
let mut p = PropsOut::new();
p.spec = NodeSpec::row().size(100.0, 40.0).on_click("skip");
p.key = Some("skip".into());
p.apply_tooltip("KEY S");
ui.core().open_from(p, Content::Box);
ui.text("SKIP", TextStyle::new(12.0));
ui.close();
let mut p = PropsOut::new();
p.spec = NodeSpec::row().size(100.0, 40.0).role(Role::Group);
p.key = Some("badge".into());
p.apply_tooltip("a hint");
ui.core().open_from(p, Content::Box);
ui.text("badge", TextStyle::new(12.0));
ui.close();
let spec = widgets::button_spec(&ui.theme(), &ui.metrics()).on_click("go");
widgets::button_with(&mut ui, "go", "Go", spec, Some("Starts it"));
ui.finish();
};
let at = |core: &mut Core, key: &str| {
let r = core.access_tree().get(Key::ROOT.str(key)).unwrap().rect;
Vec2::new(r.x + r.w / 2.0, r.y + r.h / 2.0)
};
let glyphs = |core: &mut Core| {
let (dl, _) = core.output();
dl.quads
.iter()
.filter(|q| matches!(q.kind, QuadKind::GlyphMask | QuadKind::GlyphSubpixel))
.count()
};
let read = |core: &mut Core, key: &str| {
let n = core.access_tree().get(Key::ROOT.str(key)).unwrap();
(n.role, n.name.clone(), n.description.clone())
};
let texts = |core: &mut Core| {
core.access_tree()
.nodes
.iter()
.filter(|n| n.role == Role::StaticText)
.filter_map(|n| n.name.clone())
.collect::<Vec<_>>()
};
let named = |role, name: &str, hint: &str| (role, Some(name.into()), Some(hint.into()));
frame(&mut core);
let idle = glyphs(&mut core);
let skip = named(Role::Button, "SKIP", "KEY S");
let go = named(Role::Button, "Go", "Starts it");
let badge = (Role::Group, None, Some("a hint".into()));
assert_eq!(read(&mut core, "skip"), skip, "idle");
assert_eq!(read(&mut core, "go"), go, "idle");
assert_eq!(read(&mut core, "badge"), badge, "idle");
assert_eq!(texts(&mut core), ["badge"], "idle");
for key in ["skip", "badge", "go"] {
let p = at(&mut core, key);
core.handle_input(InputEvent::CursorMoved(p));
frame(&mut core);
assert!(
glyphs(&mut core) > idle,
"the pointer over {key} draws its hint"
);
assert_eq!(read(&mut core, "skip"), skip, "pointer over {key}");
assert_eq!(read(&mut core, "go"), go, "pointer over {key}");
assert_eq!(read(&mut core, "badge"), badge, "pointer over {key}");
assert_eq!(
texts(&mut core),
["badge"],
"pointer over {key}: the hint is no text of its own"
);
}
}