use kui_core::{Core, InputEvent, Key, MouseButton, NodeSpec, Size, TextStyle, Vec2};
fn style() -> TextStyle {
TextStyle::new(14.0)
}
fn frame(core: &mut Core, range: std::ops::Range<u64>) -> Key {
frame_of(core, range, None)
}
fn frame_of(core: &mut Core, range: std::ops::Range<u64>, rows: Option<u64>) -> Key {
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
let scope = ui.with_keyed(
"list",
NodeSpec::column()
.grow_width()
.height(100.0)
.scroll_y()
.selectable(),
|ui| {
if let Some(n) = rows {
ui.row_count(n);
}
ui.leaf_keyed(
"lead",
NodeSpec::column()
.grow_width()
.height(range.start as f32 * 20.0),
);
for i in range.clone() {
ui.text_in_indexed(
i,
NodeSpec::column().grow_width().height(20.0),
&format!("row {i}"),
style(),
);
}
},
);
ui.finish();
scope
}
#[test]
fn a_selection_over_unbuilt_rows_reads_as_nothing_and_comes_back() {
let mut core = Core::new();
let scope = frame(&mut core, 0..5);
let _ = scope;
core.handle_input(InputEvent::CursorMoved(Vec2::new(2.0, 4.0)));
core.handle_input(InputEvent::MouseDown {
button: MouseButton::Primary,
clicks: 1,
});
core.handle_input(InputEvent::CursorMoved(Vec2::new(200.0, 45.0)));
core.handle_input(InputEvent::MouseUp {
button: MouseButton::Primary,
});
assert_eq!(
core.selection_text().as_deref(),
Some("row 0\nrow 1\nrow 2"),
"the rows it dragged over are built, so they select and copy"
);
frame(&mut core, 10..15);
assert!(
core.selection().is_some(),
"the selection itself survives — its ends are addresses, not places"
);
assert_eq!(
core.selection_text(),
None,
"the core cannot answer for rows it never built"
);
let tint = kui_core::select::TINT;
let painted = core
.output()
.0
.quads
.iter()
.filter(|q| q.color == tint)
.count();
assert_eq!(painted, 0, "and nothing paints, which is the visible half");
frame(&mut core, 0..5);
assert_eq!(
core.selection_text().as_deref(),
Some("row 0\nrow 1\nrow 2"),
"no drift: `open_indexed` gives a row the same key wherever it sits"
);
}
#[test]
fn the_built_middle_of_a_selection_still_paints() {
let mut core = Core::new();
frame(&mut core, 0..6);
core.handle_input(InputEvent::CursorMoved(Vec2::new(2.0, 4.0)));
core.handle_input(InputEvent::MouseDown {
button: MouseButton::Primary,
clicks: 1,
});
core.handle_input(InputEvent::CursorMoved(Vec2::new(200.0, 45.0)));
core.handle_input(InputEvent::MouseUp {
button: MouseButton::Primary,
});
let tint = kui_core::select::TINT;
let painted = |core: &mut Core| {
core.output()
.0
.quads
.iter()
.filter(|q| q.color == tint)
.count()
};
frame(&mut core, 0..6);
assert_eq!(painted(&mut core), 3, "three rows selected, three runs");
frame(&mut core, 1..7);
assert_eq!(
painted(&mut core),
2,
"the two rows still on screen keep their highlight"
);
frame(&mut core, 10..16);
assert_eq!(painted(&mut core), 0);
}
#[test]
fn a_copy_over_unbuilt_rows_asks_the_app_and_takes_its_answer() {
use kui_core::{CopyRequest, MenuAction};
let mut core = Core::new();
frame(&mut core, 0..6);
core.handle_input(InputEvent::CursorMoved(Vec2::new(2.0, 4.0)));
core.handle_input(InputEvent::MouseDown {
button: MouseButton::Primary,
clicks: 1,
});
core.handle_input(InputEvent::CursorMoved(Vec2::new(200.0, 45.0)));
core.handle_input(InputEvent::MouseUp {
button: MouseButton::Primary,
});
assert_eq!(
core.request_copy(),
CopyRequest::Ready("row 0\nrow 1\nrow 2".into())
);
frame(&mut core, 2..8);
assert_eq!(core.request_copy(), CopyRequest::Asked);
let asked = core.take_pending_events();
let ev = asked
.iter()
.find(|e| e.kind() == Some("selectionrange"))
.expect("the app is asked, on the scope");
let end = |name: &str| {
let e = ev.payload.get(name).expect("an end");
(e.get_int("index"), e.get_int("byte"))
};
assert_eq!(end("from"), (Some(0), Some(0)), "row 0, byte 0");
assert_eq!(end("to").0, Some(2), "row 2");
assert!(core.answer_selection_range("row 0\nrow 1\nrow 2"));
assert_eq!(
core.take_menu_actions(),
vec![MenuAction::SetClipboard {
text: "row 0\nrow 1\nrow 2".into(),
html: None,
}]
);
assert!(!core.answer_selection_range("stale"));
assert!(core.take_menu_actions().is_empty());
}
#[test]
fn an_end_below_a_mixed_scope_is_placed_below_it() {
let mut core = Core::new();
let build = |core: &mut Core, range: std::ops::Range<u64>| {
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
let scope = ui.with_keyed(
"list",
NodeSpec::column()
.grow_width()
.height(200.0)
.scroll_y()
.selectable(),
|ui| {
ui.text("HEADER", style());
for i in range.clone() {
ui.text_in_indexed(
i,
NodeSpec::column().grow_width().height(20.0),
&format!("row {i}"),
style(),
);
}
},
);
ui.finish();
scope
};
build(&mut core, 0..4);
core.handle_input(InputEvent::CursorMoved(Vec2::new(2.0, 4.0)));
core.handle_input(InputEvent::MouseDown {
button: MouseButton::Primary,
clicks: 1,
});
core.handle_input(InputEvent::CursorMoved(Vec2::new(200.0, 85.0)));
core.handle_input(InputEvent::MouseUp {
button: MouseButton::Primary,
});
let all = core.selection_text().expect("a selection");
assert!(all.starts_with("HEADER"), "{all:?}");
build(&mut core, 0..2);
let tint = kui_core::select::TINT;
let painted = core
.output()
.0
.quads
.iter()
.filter(|q| q.color == tint)
.count();
assert_eq!(
painted, 3,
"the header and both built rows, not a single run at the top"
);
}
#[test]
fn a_backwards_drag_asks_for_its_range_in_reading_order() {
use kui_core::CopyRequest;
let ask = |core: &mut Core| -> (i64, i64, i64, i64) {
assert_eq!(core.request_copy(), CopyRequest::Asked);
let ev = core
.take_pending_events()
.into_iter()
.find(|e| e.kind() == Some("selectionrange"))
.expect("a selectionrange ask");
let end = |name: &str| {
let e = ev.payload.get(name).unwrap();
(e.get_int("index").unwrap(), e.get_int("byte").unwrap())
};
let (from, to) = (end("from"), end("to"));
(from.0, from.1, to.0, to.1)
};
let mut core = Core::new();
frame(&mut core, 0..5);
core.handle_input(InputEvent::CursorMoved(Vec2::new(200.0, 45.0)));
core.handle_input(InputEvent::MouseDown {
button: MouseButton::Primary,
clicks: 1,
});
core.handle_input(InputEvent::CursorMoved(Vec2::new(2.0, 4.0)));
core.handle_input(InputEvent::MouseUp {
button: MouseButton::Primary,
});
assert_eq!(
core.selection_text().as_deref(),
Some("row 0\nrow 1\nrow 2")
);
let (a, f) = core.selection_ends().unwrap();
assert_eq!((a.row, f.row), (Some(2), Some(0)), "the ends are directed");
frame(&mut core, 1..6);
let (from_i, from_b, to_i, to_b) = ask(&mut core);
assert_eq!((from_i, from_b), (0, 0), "from is the earlier row");
assert_eq!((to_i, to_b), (2, 5), "to is the later, at its byte");
frame(&mut core, 10..15);
assert_eq!(ask(&mut core), (0, 0, 2, 5));
frame(&mut core, 0..2);
assert_eq!(ask(&mut core), (0, 0, 2, 5));
frame(&mut core, 0..5);
core.handle_input(InputEvent::CursorMoved(Vec2::new(2.0, 4.0)));
core.handle_input(InputEvent::MouseDown {
button: MouseButton::Primary,
clicks: 1,
});
core.handle_input(InputEvent::CursorMoved(Vec2::new(200.0, 45.0)));
core.handle_input(InputEvent::MouseUp {
button: MouseButton::Primary,
});
frame(&mut core, 10..15);
assert_eq!(ask(&mut core), (0, 0, 2, 5));
}
fn two_lists(core: &mut Core, range: std::ops::Range<u64>, other: std::ops::Range<u64>) {
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::row().fill());
ui.with_keyed(
"list",
NodeSpec::column()
.size(200.0, 140.0)
.scroll_y()
.selectable(),
|ui| {
ui.text_in_keyed(
"header",
NodeSpec::column().grow_width().height(20.0),
"header",
style(),
);
ui.leaf_keyed(
"lead",
NodeSpec::column()
.grow_width()
.height(range.start as f32 * 20.0),
);
for i in range.clone() {
ui.text_in_indexed(
i,
NodeSpec::column().grow_width().height(20.0),
&format!("row {i}"),
style(),
);
}
},
);
ui.with_keyed(
"other",
NodeSpec::column()
.size(200.0, 140.0)
.scroll_y()
.selectable(),
|ui| {
for i in other.clone() {
ui.text_in_indexed(
i,
NodeSpec::column().grow_width().height(20.0),
&format!("other {i}"),
style(),
);
}
},
);
ui.finish();
}
#[test]
fn an_unbuilt_end_is_placed_against_its_own_lists_rows() {
use kui_core::{CopyRequest, Value};
let mut core = Core::new();
two_lists(&mut core, 0..6, 0..6);
core.handle_input(InputEvent::CursorMoved(Vec2::new(2.0, 4.0)));
core.handle_input(InputEvent::MouseDown {
button: MouseButton::Primary,
clicks: 1,
});
core.handle_input(InputEvent::CursorMoved(Vec2::new(
150.0,
20.0 + 2.0 * 20.0 + 10.0,
)));
core.handle_input(InputEvent::MouseUp {
button: MouseButton::Primary,
});
assert_eq!(
core.selection_text().as_deref(),
Some("header\nrow 0\nrow 1\nrow 2")
);
two_lists(&mut core, 100..106, 0..6);
assert_eq!(core.request_copy(), CopyRequest::Asked);
let ev = core
.take_pending_events()
.into_iter()
.find(|e| e.kind() == Some("selectionrange"))
.expect("a selectionrange ask");
let index = |name: &str| {
ev.payload
.get(name)
.and_then(|e| e.get("index"))
.and_then(Value::as_int)
};
assert_eq!((index("from"), index("to")), (Some(2), None));
}
#[test]
fn select_all_spans_the_declared_rows_not_the_built_ones() {
use kui_core::CopyRequest;
use kui_core::select::ROW_END;
let mut core = Core::new();
let scope = frame(&mut core, 10..15);
assert!(core.select_all_in(scope));
assert_eq!(
core.selection_text().as_deref(),
Some("row 10\nrow 11\nrow 12\nrow 13\nrow 14")
);
assert!(matches!(core.request_copy(), CopyRequest::Ready(_)));
let mut core = Core::new();
let scope = frame_of(&mut core, 10..15, Some(2000));
assert!(core.select_all_in(scope));
let (from, to) = core.selection_range().expect("both ends in rows");
assert_eq!((from.row, from.byte), (Some(0), 0));
assert_eq!((to.row, to.byte), (Some(1999), ROW_END));
assert!(
core.selection_text().is_none(),
"the core never built row 0 or row 1999, so it cannot answer"
);
assert!(matches!(core.request_copy(), CopyRequest::Asked));
let events = core.take_pending_events();
let ask = events
.iter()
.find(|e| e.payload.get("kind").and_then(|v| v.as_str()) == Some("selectionrange"))
.expect("the ask goes out on the scope");
assert_eq!(ask.key, scope);
let end = |name: &str| {
let e = ask.payload.get(name).unwrap();
(
e.get("index").and_then(|v| v.as_int()),
e.get("byte").and_then(|v| v.as_int()),
)
};
assert_eq!(end("from"), (Some(0), Some(0)));
assert_eq!(end("to"), (Some(1999), Some(ROW_END as i64)));
frame_of(&mut core, 0..5, Some(2000));
let tint = kui_core::select::TINT;
let painted = core
.output()
.0
.quads
.iter()
.filter(|q| q.color == tint)
.count();
assert_eq!(painted, 5, "five built rows, five highlighted runs");
assert!(matches!(core.request_copy(), CopyRequest::Asked));
core.take_pending_events();
let mut core = Core::new();
let scope = frame_of(&mut core, 0..3, Some(3));
assert!(core.select_all_in(scope));
assert_eq!(
core.selection_text().as_deref(),
Some("row 0\nrow 1\nrow 2"),
"a built end is the row's own run, not a placeholder"
);
assert!(matches!(core.request_copy(), CopyRequest::Ready(_)));
let mut core = Core::new();
let scope = frame_of(&mut core, 0..0, Some(0));
assert!(!core.select_all_in(scope));
}
#[test]
fn a_select_all_end_lands_in_its_row_once_the_row_is_built() {
let mut core = Core::new();
let scope = frame_of(&mut core, 10..15, Some(2000));
assert!(core.select_all_in(scope));
core.set_scroll(scope, Vec2::new(0.0, 1995.0 * 20.0));
frame_of(&mut core, 1995..2000, Some(2000));
let tint = kui_core::select::TINT;
let painted = core
.output()
.0
.quads
.iter()
.filter(|q| q.color == tint)
.count();
assert_eq!(
painted, 5,
"five built rows at the bottom, five highlighted"
);
assert!(matches!(core.request_copy(), kui_core::CopyRequest::Asked));
}