mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-03 00:41:56 -06:00
The merge of main brought #471's code_editor_marked, whose mark was drawn by the layouter inside the code editor; on this branch that layouter lives in code_field, which had no mark, so the merge did not compile. FieldOptions gains mark (None by default), code_editor_marked passes it, and code_field underlines it as before. Test: the_marked_editor_underlines_its_mark (fails when the field ignores the mark).
483 lines
21 KiB
Rust
483 lines
21 KiB
Rust
use super::*;
|
|
use egui::Modifiers;
|
|
|
|
fn spans(text: &str, lang: Lang) -> Vec<(&str, Token)> {
|
|
highlight(text, lang).into_iter().map(|(r, k)| (text.get(r).unwrap_or("?"), k)).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn python_tokens() {
|
|
let s = spans("def f(x):\n return len(x) + 1.5 # done\nxl(\"A1:B2\")", Lang::Python);
|
|
assert_eq!(
|
|
s,
|
|
vec![
|
|
("def", Token::Keyword),
|
|
("return", Token::Keyword),
|
|
("len", Token::Builtin),
|
|
("1.5", Token::Number),
|
|
("# done", Token::Comment),
|
|
("xl", Token::Builtin),
|
|
("\"A1:B2\"", Token::String),
|
|
]
|
|
);
|
|
assert_eq!(spans("s = '''a\n'b'\nc'''", Lang::Python), vec![("'''a\n'b'\nc'''", Token::String)]);
|
|
assert_eq!(spans("x = 'it\\'s'", Lang::Python), vec![("'it\\'s'", Token::String)]);
|
|
assert_eq!(spans("hello", Lang::Plain), vec![]);
|
|
}
|
|
|
|
#[test]
|
|
fn underline_splits_sections_at_the_mark() {
|
|
let t = Tokens::light();
|
|
let font = FontId::new(12.0, FontFamily::Monospace);
|
|
let marked = |job: &egui::text::LayoutJob| -> Vec<String> {
|
|
job.sections
|
|
.iter()
|
|
.filter(|s| s.format.underline.width > 0.0)
|
|
.map(|s| job.text[s.byte_range.start.0..s.byte_range.end.0].to_string())
|
|
.collect()
|
|
};
|
|
let mut job = layout_job("a + \\foo", Lang::Latex, &font, t.text, &t.syntax, 100.0);
|
|
underline(&mut job, 4..8, t.danger);
|
|
assert_eq!(marked(&job), vec!["\\foo"]);
|
|
let total: usize = job.sections.iter().map(|s| s.byte_range.end.0 - s.byte_range.start.0).sum();
|
|
assert_eq!(total, job.text.len(), "sections still cover the text");
|
|
// An empty mark at the end underlines the last character; out-of-range marks are clamped.
|
|
let mut job = layout_job("x^", Lang::Latex, &font, t.text, &t.syntax, 100.0);
|
|
underline(&mut job, 2..2, t.danger);
|
|
assert_eq!(marked(&job), vec!["^"]);
|
|
let mut job = layout_job("\u{e9}", Lang::Latex, &font, t.text, &t.syntax, 100.0);
|
|
underline(&mut job, 50..99, t.danger);
|
|
assert!(job.sections.iter().any(|s| s.format.underline.width > 0.0));
|
|
}
|
|
|
|
#[test]
|
|
fn a_stale_mark_inside_a_multibyte_char_snaps_to_char_boundaries() {
|
|
// The editor's mark comes from the text before this frame's edit: `a\foo` had its error at
|
|
// 1..5, then an `é` (two bytes) was typed at the start, so byte 1 is inside it.
|
|
let t = Tokens::light();
|
|
let font = FontId::new(12.0, FontFamily::Monospace);
|
|
let ctx = egui::Context::default();
|
|
for text in ["\u{e9}\\foo", "\u{e9}\u{e9}\u{4e2d}x", "\u{1F600}", "a\u{e9}"] {
|
|
for start in 0..=text.len() + 1 {
|
|
for end in 0..=text.len() + 1 {
|
|
let mut job = layout_job(text, Lang::Latex, &font, t.text, &t.syntax, 200.0);
|
|
underline(&mut job, start..end, t.danger);
|
|
for s in &job.sections {
|
|
let r = s.byte_range.start.0..s.byte_range.end.0;
|
|
assert!(text.is_char_boundary(r.start) && text.is_char_boundary(r.end), "{text:?} {start}..{end}: {r:?}");
|
|
}
|
|
let total: usize = job.sections.iter().map(|s| s.byte_range.end.0 - s.byte_range.start.0).sum();
|
|
assert_eq!(total, text.len(), "sections still cover {text:?}");
|
|
// Laying the job out slices the text at every section: it must not panic.
|
|
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
|
let _ = ui.fonts_mut(|f| f.layout_job(job.clone()));
|
|
});
|
|
out.textures_delta.clear();
|
|
}
|
|
}
|
|
}
|
|
let mut job = layout_job("\u{e9}\\foo", Lang::Latex, &font, t.text, &t.syntax, 200.0);
|
|
underline(&mut job, 1..5, t.danger);
|
|
let marked: Vec<&str> =
|
|
job.sections.iter().filter(|s| s.format.underline.width > 0.0).map(|s| &job.text[s.byte_range.start.0..s.byte_range.end.0]).collect();
|
|
assert_eq!(marked.concat(), "\u{e9}\\fo", "the mark grows out to the whole characters it touches");
|
|
}
|
|
|
|
#[test]
|
|
fn latex_tokens() {
|
|
let s = spans("\\frac{a^2}{3} % half\n\\{", Lang::Latex);
|
|
assert_eq!(
|
|
s,
|
|
vec![
|
|
("\\frac", Token::Keyword),
|
|
("^", Token::Builtin),
|
|
("2", Token::Number),
|
|
("3", Token::Number),
|
|
("% half", Token::Comment),
|
|
("\\{", Token::Keyword),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_input_never_panics_and_spans_stay_on_char_boundaries() {
|
|
let inputs = [
|
|
"",
|
|
"'",
|
|
"\"",
|
|
"'''",
|
|
"\"\"\"x",
|
|
"#",
|
|
"\\",
|
|
"%",
|
|
"1",
|
|
".",
|
|
"é'ü\\",
|
|
"θ = 'π' # ∑",
|
|
"\u{1F600}'\u{1F600}",
|
|
"'\\",
|
|
"\\\u{1F600}",
|
|
"\u{200B}\n\t'''\n",
|
|
"0x_é",
|
|
"a.b.c",
|
|
"____",
|
|
];
|
|
for text in inputs {
|
|
for lang in [Lang::Python, Lang::Latex, Lang::Plain] {
|
|
let mut last = 0;
|
|
for (r, _) in highlight(text, lang) {
|
|
assert!(r.start >= last && r.start <= r.end && r.end <= text.len(), "{text:?} {lang:?} {r:?}");
|
|
assert!(text.is_char_boundary(r.start) && text.is_char_boundary(r.end), "{text:?} {lang:?} {r:?}");
|
|
last = r.end;
|
|
}
|
|
let job = layout_job(text, lang, &FontId::default(), egui::Color32::BLACK, &Tokens::light().syntax, f32::INFINITY);
|
|
assert_eq!(job.text, text, "every character is laid out once");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn enter_keeps_indentation_and_indents_after_a_colon() {
|
|
let mut s = " x = 1".to_string();
|
|
let e = newline(&mut s, (9, 9), Lang::Python);
|
|
assert_eq!(s, " x = 1\n ");
|
|
assert_eq!(e.caret, 14);
|
|
let mut s = "if x:".to_string();
|
|
let e = newline(&mut s, (5, 5), Lang::Python);
|
|
assert_eq!(s, "if x:\n ");
|
|
assert_eq!(e.caret, 10);
|
|
// Plain text keeps the indentation but adds none after a colon.
|
|
let mut s = " a:".to_string();
|
|
newline(&mut s, (4, 4), Lang::Plain);
|
|
assert_eq!(s, " a:\n ");
|
|
// A selection is replaced; out-of-range carets are clamped.
|
|
let mut s = "abcdef".to_string();
|
|
let e = newline(&mut s, (4, 2), Lang::Plain);
|
|
assert_eq!((s.as_str(), e.caret), ("ab\nef", 3));
|
|
let mut s = "é".to_string();
|
|
let e = newline(&mut s, (99, 99), Lang::Plain);
|
|
assert_eq!((s.as_str(), e.caret), ("é\n", 2));
|
|
}
|
|
|
|
#[test]
|
|
/// Shift+Tab removes four leading spaces, while a plain Tab still inserts four spaces at the caret.
|
|
fn shift_tab_in_the_editor_dedents_rather_than_indents() {
|
|
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(400.0, 300.0)).build_ui_state(
|
|
|ui, text: &mut String| {
|
|
if ui.ctx().cumulative_pass_nr() == 0 {
|
|
ui.ctx().set_fonts(theme::pinned_font_definitions());
|
|
return;
|
|
}
|
|
let id = egui::Id::unique("code");
|
|
ui.memory_mut(|m| m.request_focus(id));
|
|
code_editor(ui, id, text, Lang::Python, 5);
|
|
},
|
|
" x".to_string(),
|
|
);
|
|
h.run_steps(2);
|
|
let tab = |modifiers| egui::Event::Key { key: Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers };
|
|
h.input_mut().events.push(tab(Modifiers::SHIFT));
|
|
h.run_steps(1);
|
|
assert_eq!(h.state(), " x");
|
|
// A plain Tab still inserts spaces at the caret (the end).
|
|
h.input_mut().events.push(tab(Modifiers::NONE));
|
|
h.run_steps(1);
|
|
assert_eq!(h.state(), " x ");
|
|
}
|
|
|
|
/// Gives the context the deterministic font set, including the editor's monospace family.
|
|
fn install_mono(ctx: &egui::Context) {
|
|
ctx.set_fonts(theme::pinned_font_definitions());
|
|
}
|
|
|
|
/// Builds a 400x300 code-editor harness with pinned mono fonts, focus, two passes and an accessibility check.
|
|
fn editor_harness(text: &str) -> egui_kittest::Harness<'static, String> {
|
|
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(400.0, 300.0)).build_ui_state(
|
|
|ui, text: &mut String| {
|
|
if ui.ctx().cumulative_pass_nr() == 0 {
|
|
install_mono(ui.ctx());
|
|
return;
|
|
}
|
|
let id = egui::Id::unique("code");
|
|
ui.memory_mut(|m| m.request_focus(id));
|
|
code_editor(ui, id, text, Lang::Python, 5);
|
|
},
|
|
text.to_string(),
|
|
);
|
|
h.run_steps(2);
|
|
h.check_accessibility();
|
|
h
|
|
}
|
|
|
|
fn key(key: Key, modifiers: Modifiers) -> egui::Event {
|
|
egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }
|
|
}
|
|
|
|
#[test]
|
|
fn batched_input_applies_in_order_and_keeps_every_key() {
|
|
// Events gather between repaints (typing, key repeat). The text typed before an Enter
|
|
// comes first, and two Enters make two lines.
|
|
let mut h = editor_harness("");
|
|
h.input_mut().events.extend([egui::Event::Text("if x:".into()), key(Key::Enter, Modifiers::NONE), key(Key::Enter, Modifiers::NONE)]);
|
|
h.run_steps(1);
|
|
assert_eq!(h.state(), "if x:\n \n ");
|
|
// Backspace and Tab in the same frame as the typing around them.
|
|
h.input_mut().events.extend([
|
|
egui::Event::Text("y = 1".into()),
|
|
key(Key::Backspace, Modifiers::NONE),
|
|
egui::Event::Text("2".into()),
|
|
key(Key::Tab, Modifiers::NONE),
|
|
]);
|
|
h.run_steps(1);
|
|
assert_eq!(h.state(), "if x:\n \n y = 2 ");
|
|
// An event the field acts on (an arrow) keeps the order too: the text typed before it
|
|
// is applied first, the field moves the caret, and the Enter after it waits for the
|
|
// next pass, where the editor applies it at the moved caret, with the indentation.
|
|
h.input_mut().events.extend([egui::Event::Text("a".into()), key(Key::ArrowLeft, Modifiers::NONE), key(Key::Enter, Modifiers::NONE)]);
|
|
h.run_steps(1);
|
|
assert_eq!(h.state(), "if x:\n \n y = 2 a", "the Enter waits for the arrow");
|
|
h.run_steps(1);
|
|
assert_eq!(h.state(), "if x:\n \n y = 2 \n a");
|
|
}
|
|
|
|
#[test]
|
|
fn a_selection_ending_at_a_line_start_leaves_that_line_alone() {
|
|
// Selecting "a\n" in "a\nb" ends at the start of the second line, which is not selected.
|
|
let mut s = "a\nb".to_string();
|
|
indent(&mut s, (0, 2), false);
|
|
assert_eq!(s, " a\nb");
|
|
let mut s = " a\n b".to_string();
|
|
indent(&mut s, (0, 6), true);
|
|
assert_eq!(s, "a\n b");
|
|
// One character into the second line, both lines are touched.
|
|
let mut s = "a\nb".to_string();
|
|
indent(&mut s, (0, 3), false);
|
|
assert_eq!(s, " a\n b");
|
|
}
|
|
|
|
#[test]
|
|
fn typed_text_and_deletions_replace_the_selection() {
|
|
let mut s = "abcd".to_string();
|
|
assert_eq!(insert(&mut s, (3, 1), "X"), Edit { anchor: 2, caret: 2 });
|
|
assert_eq!(s, "aXd");
|
|
assert_eq!(delete(&mut s, (2, 2), true), Edit { anchor: 1, caret: 1 });
|
|
assert_eq!(s, "ad");
|
|
assert_eq!(delete(&mut s, (1, 1), false), Edit { anchor: 1, caret: 1 });
|
|
assert_eq!(s, "a");
|
|
assert_eq!(delete(&mut s, (0, 0), true), Edit { anchor: 0, caret: 0 });
|
|
assert_eq!(delete(&mut s, (9, 9), false), Edit { anchor: 1, caret: 1 });
|
|
assert_eq!(s, "a");
|
|
let mut s = "éa".to_string();
|
|
delete(&mut s, (2, 0), true);
|
|
assert_eq!(s, "");
|
|
// The queue keeps what the editor does not handle, and everything after it.
|
|
let mut events =
|
|
vec![egui::Event::Text("x".into()), egui::Event::Text("\n".into()), key(Key::ArrowLeft, Modifiers::NONE), key(Key::Enter, Modifiers::NONE)];
|
|
let (mut s, mut sel) = (String::new(), (0, 0));
|
|
let out = apply_in_order(&mut events, &mut s, &mut sel, Lang::Python);
|
|
assert!(out.edited);
|
|
assert_eq!((s.as_str(), sel), ("x", (1, 1)));
|
|
assert_eq!(events.len(), 2, "the lone newline and the arrow stay for the field: {events:?}");
|
|
assert_eq!(out.held, vec![key(Key::Enter, Modifiers::NONE)], "the Enter after the arrow waits for the next pass");
|
|
let mut events = vec![egui::Event::Key { key: Key::Enter, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE }];
|
|
let out = apply_in_order(&mut events, &mut s, &mut sel, Lang::Python);
|
|
assert!(!out.edited && out.held.is_empty(), "a key release is nothing to apply");
|
|
assert_eq!(events.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn select_all_then_tab_in_one_pass_indents_every_line() {
|
|
// Cmd+A is the field's, Tab the editor's: the Tab waits for the pass after the
|
|
// selection and indents the selected lines, rather than reaching the field, which would
|
|
// replace the selection with a tab character.
|
|
let mut h = editor_harness("a\nb");
|
|
h.input_mut().events.extend([key(Key::A, Modifiers::COMMAND), key(Key::Tab, Modifiers::NONE)]);
|
|
h.run_steps(1);
|
|
assert_eq!(h.state(), "a\nb", "the Tab waits for the selection");
|
|
h.run_steps(1);
|
|
assert_eq!(h.state(), " a\n b");
|
|
h.input_mut().events.extend([key(Key::A, Modifiers::COMMAND), key(Key::Tab, Modifiers::SHIFT)]);
|
|
h.run_steps(2);
|
|
assert_eq!(h.state(), "a\nb");
|
|
}
|
|
|
|
#[test]
|
|
fn a_keystroke_sequence_as_the_platform_sends_it_keeps_the_editors_enter() {
|
|
// egui-winit sends a Key event before each Text event. Those presses are nobody's to
|
|
// read, so they do not hand the Enter to the field, which knows no indentation.
|
|
let mut h = editor_harness("");
|
|
let typed = |k, c: &str, m| [key(k, m), egui::Event::Text(c.into())];
|
|
let mut events = Vec::new();
|
|
events.extend(typed(Key::I, "i", Modifiers::NONE));
|
|
events.extend(typed(Key::F, "f", Modifiers::NONE));
|
|
events.extend(typed(Key::Space, " ", Modifiers::NONE));
|
|
events.extend(typed(Key::X, "x", Modifiers::NONE));
|
|
events.extend(typed(Key::Colon, ":", Modifiers::SHIFT));
|
|
events.push(key(Key::Enter, Modifiers::NONE));
|
|
h.input_mut().events.extend(events);
|
|
h.run_steps(1);
|
|
assert_eq!(h.state(), "if x:\n ");
|
|
// A character typed with Option (macOS) comes as a key under that modifier and its text;
|
|
// the field reads neither the key nor Option, so the Enter after it is still the editor's.
|
|
h.input_mut().events.extend([key(Key::E, Modifiers::ALT), egui::Event::Text("\u{e9}".into()), key(Key::Enter, Modifiers::NONE)]);
|
|
h.run_steps(1);
|
|
assert_eq!(h.state(), "if x:\n \u{e9}\n ");
|
|
}
|
|
|
|
#[test]
|
|
fn tab_and_enter_under_modifiers_never_reach_the_field() {
|
|
// The field would make a tab character of Ctrl+Tab (in place of the selection) and a
|
|
// newline of Shift+Enter; Cmd+Enter is nobody's here and stays for a shortcut.
|
|
let mut events = vec![key(Key::Tab, Modifiers::CTRL), key(Key::Enter, Modifiers::SHIFT), key(Key::Enter, Modifiers::COMMAND)];
|
|
let (mut s, mut sel) = ("if x:".to_string(), (5, 5));
|
|
let out = apply_in_order(&mut events, &mut s, &mut sel, Lang::Python);
|
|
assert_eq!(s, "if x:\n ");
|
|
assert_eq!(events, vec![key(Key::Enter, Modifiers::COMMAND)]);
|
|
assert!(out.held.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn a_stop_key_ends_the_pass_in_input_order() {
|
|
let stop = |ev: &egui::Event| matches!(ev, egui::Event::Key { key: Key::Enter, pressed: true, modifiers, .. } if modifiers.command);
|
|
// The typing before it is applied; the keys after it are dropped, other input stays.
|
|
let mut events = vec![
|
|
egui::Event::Text("a".into()),
|
|
key(Key::Enter, Modifiers::COMMAND),
|
|
egui::Event::Text("b".into()),
|
|
egui::Event::PointerMoved(egui::Pos2::ZERO),
|
|
key(Key::Tab, Modifiers::NONE),
|
|
];
|
|
let (mut s, mut sel) = (String::new(), (0, 0));
|
|
let out = apply_in_order_until(&mut events, &mut s, &mut sel, Lang::Python, &stop);
|
|
assert_eq!(s, "a");
|
|
assert_eq!(out.stop, Some(key(Key::Enter, Modifiers::COMMAND)));
|
|
assert_eq!(events, vec![egui::Event::PointerMoved(egui::Pos2::ZERO)]);
|
|
assert!(out.held.is_empty());
|
|
// After a key the field acts on, it waits for the next pass with the editor's keys
|
|
// before it, so the edit it ends includes them.
|
|
let mut events = vec![key(Key::A, Modifiers::COMMAND), key(Key::Tab, Modifiers::NONE), key(Key::Enter, Modifiers::COMMAND)];
|
|
let out = apply_in_order_until(&mut events, &mut s, &mut sel, Lang::Python, &stop);
|
|
assert_eq!(out.stop, None);
|
|
assert_eq!(events, vec![key(Key::A, Modifiers::COMMAND)]);
|
|
assert_eq!(out.held, vec![key(Key::Tab, Modifiers::NONE), key(Key::Enter, Modifiers::COMMAND)]);
|
|
// Next pass: the Tab, then the stop.
|
|
let mut events = out.held;
|
|
let out = apply_in_order_until(&mut events, &mut s, &mut sel, Lang::Python, &stop);
|
|
assert_eq!(s, "a ");
|
|
assert_eq!(out.stop, Some(key(Key::Enter, Modifiers::COMMAND)));
|
|
// Without a stop the editor behaves as before: the caller's key is the field's.
|
|
let mut events = vec![key(Key::Enter, Modifiers::COMMAND)];
|
|
let out = apply_in_order(&mut events, &mut s, &mut sel, Lang::Python);
|
|
assert_eq!((out.stop, events.len()), (None, 1));
|
|
}
|
|
|
|
#[test]
|
|
fn events_after_a_held_key_wait_with_it_and_pointer_events_do_not() {
|
|
let mut events = vec![
|
|
key(Key::X, Modifiers::NONE),
|
|
egui::Event::Text("x".into()),
|
|
key(Key::A, Modifiers::COMMAND),
|
|
key(Key::Tab, Modifiers::NONE),
|
|
egui::Event::PointerMoved(egui::Pos2::ZERO),
|
|
key(Key::ArrowLeft, Modifiers::NONE),
|
|
egui::Event::Text("y".into()),
|
|
];
|
|
let (mut s, mut sel) = (String::new(), (0, 0));
|
|
let out = apply_in_order(&mut events, &mut s, &mut sel, Lang::Python);
|
|
assert_eq!((s.as_str(), sel), ("x", (1, 1)), "the typing before the select-all is applied");
|
|
assert_eq!(events, vec![key(Key::X, Modifiers::NONE), key(Key::A, Modifiers::COMMAND), egui::Event::PointerMoved(egui::Pos2::ZERO)]);
|
|
assert_eq!(out.held, vec![key(Key::Tab, Modifiers::NONE), key(Key::ArrowLeft, Modifiers::NONE), egui::Event::Text("y".into())]);
|
|
}
|
|
|
|
#[test]
|
|
/// Events held by the previous pass are applied once in the next pass; older held events are dropped and cleared.
|
|
fn held_back_events_land_in_the_next_pass_only() {
|
|
let mut h = editor_harness("");
|
|
let id = egui::Id::unique("code");
|
|
let held = |pass, c: &str| Held { pass, events: vec![egui::Event::Text(c.into())] };
|
|
// What the pass just completed held back comes in with the next one.
|
|
let last = h.ctx.cumulative_pass_nr() - 1;
|
|
h.ctx.data_mut(|d| d.insert_temp(held_id(id), held(last, "a")));
|
|
h.run_steps(1);
|
|
assert_eq!(h.state(), "a");
|
|
// Anything older is dropped rather than typed out of context, and cleared.
|
|
h.ctx.data_mut(|d| d.insert_temp(held_id(id), held(last, "b")));
|
|
h.run_steps(1);
|
|
assert_eq!(h.state(), "a");
|
|
assert!(h.ctx.data(|d| d.get_temp::<Held>(held_id(id))).is_none());
|
|
}
|
|
|
|
#[test]
|
|
/// Held keyboard input does not land in the next focused text field once the code editor has lost focus.
|
|
fn held_back_events_go_nowhere_once_the_editor_lost_the_focus() {
|
|
// Typed into the editor, they must not land in whatever has the focus next.
|
|
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(400.0, 300.0)).build_ui_state(
|
|
|ui, s: &mut (String, String, bool)| {
|
|
if ui.ctx().cumulative_pass_nr() == 0 {
|
|
install_mono(ui.ctx());
|
|
return;
|
|
}
|
|
let (code, other) = (egui::Id::unique("code"), egui::Id::unique("other"));
|
|
ui.memory_mut(|m| m.request_focus(if s.2 { code } else { other }));
|
|
code_editor(ui, code, &mut s.0, Lang::Python, 5);
|
|
ui.add(TextEdit::singleline(&mut s.1).id(other)).accessible_name("Other field");
|
|
},
|
|
(String::new(), String::new(), true),
|
|
);
|
|
h.run_steps(2);
|
|
h.input_mut().events.extend([key(Key::ArrowLeft, Modifiers::NONE), egui::Event::Text("x".into())]);
|
|
h.run_steps(1);
|
|
assert_eq!(h.state().0, "", "the x waits for the arrow");
|
|
h.state_mut().2 = false;
|
|
h.run_steps(2);
|
|
assert_eq!((h.state().0.as_str(), h.state().1.as_str()), ("", ""));
|
|
}
|
|
|
|
#[test]
|
|
/// A discard-triggered repeat pass receives held keys with the selection made by the first pass.
|
|
fn a_repeat_pass_of_the_frame_counts_as_the_next_pass() {
|
|
// When a widget asks egui to discard and repeat the pass, the repeat pass gets the
|
|
// held-back keys, with the selection the field made in the first pass.
|
|
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(400.0, 300.0)).build_ui_state(
|
|
|ui, s: &mut (String, bool)| {
|
|
if ui.ctx().cumulative_pass_nr() == 0 {
|
|
install_mono(ui.ctx());
|
|
return;
|
|
}
|
|
let id = egui::Id::unique("code");
|
|
ui.memory_mut(|m| m.request_focus(id));
|
|
code_editor(ui, id, &mut s.0, Lang::Python, 5);
|
|
if std::mem::take(&mut s.1) {
|
|
ui.ctx().request_discard("the test repeats this pass");
|
|
}
|
|
},
|
|
("a\nb".to_string(), false),
|
|
);
|
|
h.run_steps(2);
|
|
h.state_mut().1 = true;
|
|
h.input_mut().events.extend([key(Key::A, Modifiers::COMMAND), key(Key::Tab, Modifiers::NONE)]);
|
|
h.run_steps(1);
|
|
assert_eq!(h.state().0, " a\n b");
|
|
}
|
|
|
|
/// Whether a frame drew text with an underline.
|
|
fn draws_underline(mark: Option<std::ops::Range<usize>>) -> bool {
|
|
let ctx = egui::Context::default();
|
|
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
|
let mut text = String::from("x^{2");
|
|
code_editor_marked(ui, egui::Id::unique("marked"), &mut text, Lang::Latex, 3, mark.clone());
|
|
});
|
|
out.textures_delta.clear();
|
|
out.shapes.iter().any(|s| match &s.shape {
|
|
egui::Shape::Text(t) => t.galley.job.sections.iter().any(|sec| sec.format.underline.width > 0.0),
|
|
_ => false,
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn the_marked_editor_underlines_its_mark() {
|
|
// The equation editor marks an error's position; the code field draws it.
|
|
assert!(draws_underline(Some(3..4)));
|
|
assert!(!draws_underline(None));
|
|
}
|