diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index af588e6aaf..e5f2999229 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -595,9 +595,9 @@ impl eframe::App for PdfCraftApp { view.objects.set_input_blocked(shut); if shut { view.auto_scroll.cancel(); - // Edit text ignores input while it is blocked: its drags end rather than wait, to + // Edit text and Add text's drags end while their input is blocked rather than wait, to // land wherever the pointer is once it opens again. - if self.quick_tool == QuickTool::EditText { + if matches!(self.quick_tool, QuickTool::EditText | QuickTool::AddText) { view.cancel_edit_text_gestures(); } } diff --git a/crates/ui-egui/src/canvas/page_input.rs b/crates/ui-egui/src/canvas/page_input.rs index 0df0ebb5ea..42ff99022b 100644 --- a/crates/ui-egui/src/canvas/page_input.rs +++ b/crates/ui-egui/src/canvas/page_input.rs @@ -129,13 +129,12 @@ pub(super) fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut let wants_under = editing || resp.clicked() || resp.secondary_clicked() || matches!(tool, QuickTool::Fill(_)) && !ptr.any_down; let under = pointer.filter(|p| wants_under && xf.rect.contains(*p)).map(|p| hits.at(view, p, None)).unwrap_or_default(); let hovered = top(under.iter().cloned(), tool); - // A double-click on the objects selected together is the object's under it: the double-click - // leaves the group (`object_ui`), and added text opens for typing, a picture or a paragraph is - // the one taken (the user's ruling). + // A double-click on the objects selected together is the object's its press was on: the + // double-click leaves the group (`object_ui`), and added text opens for typing, a picture or a + // paragraph is the one taken (the user's ruling). let press = match press { PagePress::Mine(Owner::Objects { part: crate::page_router::ObjectsPart::Selection }) if resp.double_clicked() => { - top(under.iter().filter(|h| !matches!(h.owner, Owner::Objects { .. })).cloned(), tool) - .map_or(PagePress::Free, |h| PagePress::Mine(h.owner)) + view.router.under_objects().map_or(PagePress::Free, PagePress::Mine) } press => press, }; @@ -825,8 +824,11 @@ pub(super) fn page_overlays(ui: &mut egui::Ui, view: &mut DocView, frame: &mut P let tool_cx = PageToolContext { ui, resp, xf: &xf, ptr, page: i, doc, info }; let pcx = comments::PageCx { page: i, xf: &xf, ptr, info, tool, prefs, allowed, hidden: comments_hidden, redact_mark }; // A press nobody holds picks the picture under it: a link's click follows the link only (ruling - // 5), though its drag or double-click still lets the picked picture go. - if tool == QuickTool::Select && matches!(input.press, PagePress::Free | PagePress::Mine(Owner::Link { .. })) && !input.consumed { + // 5), though its drag or double-click still lets the picked picture go, as does a drag the page + // it is over first sees, whichever page it began on. + let pick_sees = + matches!(input.press, PagePress::Free | PagePress::Mine(Owner::Link { .. })) || input.press == PagePress::Elsewhere && ptr.drag_started; + if tool == QuickTool::Select && pick_sees && !input.consumed { crate::image_pick::page_input(resp, &xf, i, info, doc, ptr, input.press == PagePress::Free, view); } crate::image_pick::drop_stale(doc, view); diff --git a/crates/ui-egui/src/canvas/page_router.rs b/crates/ui-egui/src/canvas/page_router.rs index 463f315e0c..30a3f2126f 100644 --- a/crates/ui-egui/src/canvas/page_router.rs +++ b/crates/ui-egui/src/canvas/page_router.rs @@ -175,6 +175,9 @@ impl PagePress { struct Capture { pub owner: Owner, pub declines_drag: bool, + /// What lay on top at the press with the objects selected together aside: a double-click on + /// them reaches it. + under_objects: Option, } /// The press the pages hold, from its frame until the button is up. @@ -202,7 +205,9 @@ impl PageRouter { if self.began != Some(None) { return; } - self.capture = top(hits, tool).map(|h| Capture { owner: h.owner, declines_drag: h.declines_drag }); + let hits: Vec = hits.into_iter().collect(); + let under_objects = top(hits.iter().filter(|h| !matches!(h.owner, Owner::Objects { .. })).cloned(), tool).map(|h| h.owner); + self.capture = top(hits, tool).map(|h| Capture { owner: h.owner, declines_drag: h.declines_drag, under_objects }); self.began = Some(Some(page)); } @@ -216,6 +221,11 @@ impl PageRouter { } } + /// What lay on top at the press with the objects selected together aside. + pub(in crate::canvas) fn under_objects(&self) -> Option { + self.capture.as_ref().and_then(|c| c.under_objects) + } + /// Whether the press is held by someone who keeps its drag: then the page's text is not /// selected by it. pub(in crate::canvas) fn keeps_drag(&self) -> bool { diff --git a/crates/ui-egui/src/tools/content_ui.rs b/crates/ui-egui/src/tools/content_ui.rs index 581b102a41..a5cfbd4c2e 100644 --- a/crates/ui-egui/src/tools/content_ui.rs +++ b/crates/ui-egui/src/tools/content_ui.rs @@ -110,6 +110,8 @@ pub struct ContentView { grab: Option<(usize, Grab, Pos2)>, /// Where the press nobody holds began, on which page: Add text's click starts its new box there. pressed_at: Option<(usize, Pos2)>, + /// A button is down on the pages this frame (for [`keys`], which runs after them). + held: bool, undrawable: UndrawableMemo, layout: Option<(AddedText, f32, f64, [f64; 4])>, } @@ -404,6 +406,7 @@ pub(crate) struct ContentCx<'a> { pub(crate) fn page_input(cx: PageToolContext<'_>, ccx: ContentCx<'_>, view: &mut DocView) -> bool { let PageToolContext { resp, xf, ptr, page, info, .. } = cx; let ContentCx { added, press, gate, picks_up, adding_text, edit_on_click, style } = ccx; + view.content.held = ptr.any_down; if !gate { return false; } @@ -634,7 +637,11 @@ pub(crate) fn keys(ctx: &egui::Context, view: &mut DocView, image_controller_act return; } let (del, esc) = ctx.input(|i| (i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace), i.key_pressed(egui::Key::Escape))); - if del && let Some((page, index)) = view.content.selected.take() { + // Not while a button is down: the press held would then name another item. + if del + && !view.content.held + && let Some((page, index)) = view.content.selected.take() + { view.pending_edit = Some(Edit::DeleteContent { page, index }); } else if esc { view.content.selected = None; diff --git a/crates/ui-egui/src/tools/edit_text_ui/image.rs b/crates/ui-egui/src/tools/edit_text_ui/image.rs index b63f876298..07ca017798 100644 --- a/crates/ui-egui/src/tools/edit_text_ui/image.rs +++ b/crates/ui-egui/src/tools/edit_text_ui/image.rs @@ -467,14 +467,15 @@ fn apply_crop_mode(view: &mut DocView, image: &pdfcraft_engine::PageImage) { } } -/// The keys of the picture selected on `page`, a page being drawn, so the keys never reach a picture -/// off the screen, and the page grid and the Pages panel have had theirs. Delete or Backspace removes -/// it, wherever on the pages the pointer is, unless it is being dragged or cropped. In crop mode +/// The keys of the picture selected on `page`, a page being drawn, so the page grid and the Pages +/// panel have had theirs; nothing while the page is off the screen, as pages near it are drawn too. +/// Delete or Backspace removes it, wherever on the pages the pointer is, unless it is being cropped or +/// a button is down: the press held would then name another picture. In crop mode /// Enter applies the crop and Escape leaves it, though not while a button is down (`held`): Escape /// then cancels the press the pages hold, if any (`canvas::cancel_on_escape`), and crop mode stays. /// Nothing while a text field has the keyboard. pub(crate) fn picture_keys(ctx: &egui::Context, view: &mut DocView, page: usize, images: &[pdfcraft_engine::PageImage], held: bool) { - if ctx.egui_wants_keyboard_input() { + if ctx.egui_wants_keyboard_input() || view.page_screen_rect(page).is_none() { return; } let Some(s) = view.image_selection.as_ref().filter(|s| s.page == page) else { return }; @@ -492,7 +493,7 @@ pub(crate) fn picture_keys(ctx: &egui::Context, view: &mut DocView, page: usize, { s.crop = None; } - } else if !s.dragging() && !s.turning && (key(egui::Key::Delete) || key(egui::Key::Backspace)) && image.is_some() { + } else if !held && !s.dragging() && !s.turning && (key(egui::Key::Delete) || key(egui::Key::Backspace)) && image.is_some() { // The key is the picture's: added content's own Delete runs later this frame. ctx.input_mut(|i| { i.consume_key(egui::Modifiers::NONE, egui::Key::Delete); diff --git a/crates/ui-egui/src/tools/object_ui.rs b/crates/ui-egui/src/tools/object_ui.rs index b57f3ad79b..0ecfb6d29b 100644 --- a/crates/ui-egui/src/tools/object_ui.rs +++ b/crates/ui-egui/src/tools/object_ui.rs @@ -340,7 +340,9 @@ fn input(s: &mut ObjectSelection, cx: PageToolContext<'_>, press: PagePress, vie let Some(p) = pointer.filter(|p| xf.rect.contains(*p) && ui.clip_rect().contains(*p) && resp.rect.contains(*p)) else { return s.page == Some(page) && !s.selected.is_empty(); }; - if !pressed { + // Nor a press another page holds, on the row of pixels pages share with the gaps hidden: it may + // have just selected something there. + if !pressed || press == PagePress::Elsewhere { return s.page == Some(page) && !s.selected.is_empty(); } let Ok(objects) = s.inventory(doc, page) else { return false }; diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index d71e3bfa85..aaad18b48e 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -7485,6 +7485,7 @@ fn add_text_moves_no_paragraph_while_it_holds_a_draft_it_cant_draw() { h.event(egui::Event::Text("Hello 世界".into())); h.run_steps(2); assert_eq!(h.state().views[0].content.draft.as_ref().map(|d| d.text.as_str()), Some("Hello 世界"), "the draft holds the text"); + let rect = h.state().views[0].content.draft.as_ref().map(|d| d.rect).unwrap(); let on_text = point_on(&h, (block.rect[0] + block.rect[2]) / 2.0, (block.rect[1] + block.rect[3]) / 2.0); h.hover_at(on_text); h.run_steps(1); @@ -7497,6 +7498,8 @@ fn add_text_moves_no_paragraph_while_it_holds_a_draft_it_cant_draw() { h.drop_at(on_text + egui::vec2(40.0, 0.0)); h.run_steps(3); assert_eq!(h.state().session.get(id).unwrap().text_blocks(0)[0].rect, block.rect, "the paragraph stays"); + let draft = h.state().views[0].content.draft.as_ref().map(|d| (d.text.clone(), d.rect)); + assert_eq!(draft, Some(("Hello 世界".to_string(), rect)), "and the draft stays where it is typed"); } /// With Edit text, a right-click on a comment over a picture opens the comment's menu, and the @@ -7554,6 +7557,48 @@ fn a_crop_grip_takes_a_press_within_its_reach() { assert_ne!(keep(&h), before, "the grip takes the drag"); } +/// With Add text, a click on another paragraph keeps both what was typed in the open paragraph and +/// the text being added: neither edit takes the other's place. +#[test] +fn add_text_keeps_a_changed_paragraph_and_a_draft_when_it_moves_on() { + let mut h = picture_pages(false); + h.state_mut().views[0].image_selection = None; + assert!(h.state_mut().execute("edit.text")); + h.run_steps(3); + let id = h.state().views[0].id; + let blocks = h.state().session.get(id).unwrap().text_blocks(0); + assert!(blocks.len() >= 2, "two paragraphs: {blocks:?}"); + let centre = |r: [f64; 4]| ((r[0] + r[2]) / 2.0, (r[1] + r[3]) / 2.0); + let (ax, ay) = centre(blocks[0].rect); + let (bx, by) = centre(blocks[1].rect); + let a = page_at(&h, 0, ax as f32, ay as f32); + let b = page_at(&h, 0, bx as f32, by as f32); + slipping_click(&mut h, a, a); + assert!(h.state().views[0].line_editor.is_some(), "the first paragraph is open"); + h.event(egui::Event::Text("X".into())); + h.run_steps(3); + // A click on the page's empty part, below the picture, starts a box. + let blank = page_at(&h, 0, 180.0, 20.0); + slipping_click(&mut h, blank, blank); + assert!(h.state().views[0].line_editor.is_none(), "that click applied the paragraph"); + h.event(egui::Event::Text("Added words".into())); + h.run_steps(3); + slipping_click(&mut h, b, b); + h.run_steps(4); + let doc = h.state().session.get(id).unwrap(); + let texts: Vec = doc.text_blocks(0).iter().map(|b| b.text.clone()).collect(); + let added: Vec = doc + .added() + .iter() + .filter_map(|a| match &a.content { + pdfcraft_engine::AddedContent::Text(t) => Some(t.text.clone()), + _ => None, + }) + .collect(); + assert!(added.iter().any(|t| t == "Added words"), "the added text is kept: {added:?}"); + assert!(texts.iter().any(|t| t.contains('X')), "the paragraph's typing is kept: {texts:?}"); +} + /// In the page grid, Delete deletes the page chosen there. The picture selected before the grid /// opened is not on screen, and stays. #[test] @@ -7598,6 +7643,91 @@ fn a_selected_pictures_keys_wait_for_its_page_to_be_on_screen() { assert_eq!(after.len(), before.len(), "the picture stays"); assert_eq!(after[0].visible_rect, before[0].visible_rect, "and is not cropped"); } + // A page just off the screen, in a continuous view: it is still run for what lies near it. + let mut h = picture_pages(false); + let id = h.state().views[0].id; + let pictures = h.state().session.get(id).unwrap().page_images(0).len(); + h.set_size(egui::vec2(1400.0, 400.0)); + h.state_mut().views[0].go_to_page(1); + h.run_steps(6); + // Going to a page leaves a margin above it: a little further, past page 1's foot. + let centre = h.state().views[0].viewport_rect().center(); + h.hover_at(centre); + h.run_steps(1); + h.event(egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: egui::vec2(0.0, -60.0), + phase: egui::TouchPhase::Move, + modifiers: egui::Modifiers::NONE, + source: egui::MouseWheelSource::Trackpad, + }); + h.run_steps(6); + assert!(h.state().views[0].continuous, "a continuous view"); + assert!(h.state().views[0].page_screen_rect(0).is_none(), "page 1 is off the screen"); + h.key_press(egui::Key::Delete); + h.run_steps(4); + assert_eq!(h.state().session.get(id).unwrap().page_images(0).len(), pictures, "the picture stays"); +} + +/// Delete waits for the button to come up: the press held would then name another item, whose drag or +/// click would follow. +#[test] +fn delete_waits_for_the_button_to_come_up() { + let hold_and_delete = |h: &mut Harness<'static, PdfCraftApp>, on: egui::Pos2| { + h.hover_at(on); + h.run_steps(1); + h.drag_at(on); + h.run_steps(1); + h.key_press(egui::Key::Delete); + h.run_steps(2); + h.drop_at(on); + h.run_steps(3); + }; + let mut h = picture_pages(false); + let id = h.state().views[0].id; + let pictures = h.state().session.get(id).unwrap().page_images(0).len(); + let on_picture = page_at(&h, 0, 100.0, 180.0); + hold_and_delete(&mut h, on_picture); + assert_eq!(h.state().session.get(id).unwrap().page_images(0).len(), pictures, "the picture stays"); + + let mut h = edit_page(pdfcraft_ui_egui::QuickTool::Select); + assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::AddText { + page: 0, + text: pdfcraft_engine::AddedText { rect: [40.0, 250.0, 140.0, 265.0], text: "Added".into(), ..Default::default() }, + })); + assert!(h.state_mut().execute("edit.text")); + h.run_steps(3); + h.state_mut().quick_tool = pdfcraft_ui_egui::QuickTool::Select; + h.run_steps(3); + let on_added = point_on(&h, 90.0, 258.0); + slipping_click(&mut h, on_added, on_added); + assert_eq!(h.state().views[0].content.selected, Some((0, 0)), "the added text is selected"); + hold_and_delete(&mut h, on_added); + let id = h.state().views[0].id; + assert_eq!(h.state().session.get(id).unwrap().added().len(), 1, "the added text stays"); +} + +/// A drag the page it is over first sees lets the picked picture go, whichever page it began on. +#[test] +fn a_drag_first_seen_on_the_next_page_lets_the_picked_picture_go() { + let mut h = picture_pages(false); + h.state_mut().views[0].image_selection = None; + h.state_mut().quick_tool = pdfcraft_ui_egui::QuickTool::Select; + h.run_steps(3); + let on_picture = page_at(&h, 0, 100.0, 180.0); + slipping_click(&mut h, on_picture, on_picture); + assert!(h.state().views[0].picked_image.is_some(), "the picture is picked"); + let press = page_at(&h, 0, 180.0, 20.0); + let release = page_at(&h, 1, 180.0, 280.0); + h.hover_at(press); + h.run_steps(1); + h.drag_at(press); + h.run_steps(1); + h.hover_at(release); + h.run_steps(1); + h.drop_at(release); + h.run_steps(4); + assert!(h.state().views[0].picked_image.is_none(), "the drag lets it go"); } /// With the pointer on the Pages panel, Delete is the panel's: a page goes, though a picture is @@ -7880,29 +8010,31 @@ fn an_add_text_click_pressed_beside_a_paragraph_starts_a_box_there() { /// waited, and moved the paragraph to wherever the pointer then was. #[test] fn a_paragraph_drag_ends_when_the_window_loses_focus() { - let mut h = edit_page(pdfcraft_ui_egui::QuickTool::EditText); - let id = h.state().views[0].id; - let before = h.state().session.get(id).unwrap().text_blocks(0)[0].rect; - let from = point_on(&h, (before[0] + before[2]) / 2.0, (before[1] + before[3]) / 2.0); - h.hover_at(from); - h.run_steps(1); - h.drag_at(from); - h.run_steps(1); - for k in 1..=4 { - h.hover_at(from + egui::vec2(0.0, 10.0 * k as f32)); + for tool in [pdfcraft_ui_egui::QuickTool::EditText, pdfcraft_ui_egui::QuickTool::AddText] { + let mut h = edit_page(tool); + let id = h.state().views[0].id; + let before = h.state().session.get(id).unwrap().text_blocks(0)[0].rect; + let from = point_on(&h, (before[0] + before[2]) / 2.0, (before[1] + before[3]) / 2.0); + h.hover_at(from); h.run_steps(1); + h.drag_at(from); + h.run_steps(1); + for k in 1..=4 { + h.hover_at(from + egui::vec2(0.0, 10.0 * k as f32)); + h.run_steps(1); + } + assert!(h.state().views[0].block_drag.is_some(), "the paragraph is being moved"); + h.input_mut().focused = false; + h.run_steps(2); + h.drop_at(from + egui::vec2(0.0, 40.0)); + h.run_steps(2); + h.input_mut().focused = true; + h.hover_at(from + egui::vec2(0.0, 80.0)); + h.run_steps(4); + assert!(h.state().views[0].block_drag.is_none(), "the drag is over"); + assert_eq!(h.state().session.get(id).unwrap().text_blocks(0)[0].rect, before, "nothing moved"); + assert!(!dirty(&h)); } - assert!(h.state().views[0].block_drag.is_some(), "the paragraph is being moved"); - h.input_mut().focused = false; - h.run_steps(2); - h.drop_at(from + egui::vec2(0.0, 40.0)); - h.run_steps(2); - h.input_mut().focused = true; - h.hover_at(from + egui::vec2(0.0, 80.0)); - h.run_steps(4); - assert!(h.state().views[0].block_drag.is_none(), "the drag is over"); - assert_eq!(h.state().session.get(id).unwrap().text_blocks(0)[0].rect, before, "nothing moved"); - assert!(!dirty(&h)); } /// The rotate knob of a picture that fills its page lies past the page's edge, and a drag there diff --git a/crates/ui-egui/tests/it/object_selection.rs b/crates/ui-egui/tests/it/object_selection.rs index e19f939e2e..21ebc90b69 100644 --- a/crates/ui-egui/tests/it/object_selection.rs +++ b/crates/ui-egui/tests/it/object_selection.rs @@ -452,6 +452,47 @@ fn a_double_click_on_a_grouped_object_reaches_it() { assert!(h.state().views[0].content.draft.as_ref().is_some_and(|d| d.index == Some(0)), "the added note opens for typing"); } +/// A double-click on a paragraph selected with others opens the one its press was on, though the +/// button came up just off it. +#[test] +fn a_double_click_on_a_grouped_paragraph_released_just_off_it_opens_it() { + let mut h = harness(); + marquee(&mut h); + let (inside, outside) = { + let app = h.state(); + let view = &app.views[0]; + let xf = view.page_xform(0).unwrap(); + let doc = app.session.get(view.id).unwrap(); + let block = doc.text_blocks(0).into_iter().find(|b| b.text.contains("Group headline")).expect("the headline"); + let frame = xf.user_rect(&doc.info, 0, block.rect.map(|v| v as f32)); + (egui::pos2(frame.center().x, frame.top() + 1.0), egui::pos2(frame.center().x, frame.top() - 4.0)) + }; + let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE }; + h.hover_at(inside); + h.run_steps(1); + crate::support::input::one_frame(&mut h, [button(inside, true), button(inside, false)]); + crate::support::input::one_frame(&mut h, [button(inside, true), egui::Event::PointerMoved(outside), button(outside, false)]); + h.run_steps(2); + assert!(h.state().views[0].line_editor.is_some(), "the paragraph pressed is open for typing"); +} + +/// A press counts its own modifiers, whatever the frame's are: Shift in the press event adds to the +/// selection, though the frame's modifiers show none. +#[test] +fn a_press_counts_the_modifiers_of_its_own_event() { + let mut h = harness(); + let p = at(&h, 55.0, 70.0); + click(&mut h, p, Modifiers::NONE); + assert!(h.state().views[0].line_editor.is_some()); + let p = at(&h, 60.0, 145.0); + let button = |pressed| egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::SHIFT }; + h.hover_at(p); + h.run_steps(2); + crate::support::input::one_frame(&mut h, [button(true), button(false)]); + h.run_steps(4); + h.get_by_label("2 objects selected"); +} + /// A double-click on a paragraph selected with others opens it for typing (the user's ruling). #[test] fn a_double_click_on_a_grouped_paragraph_opens_it() { diff --git a/crates/ui-egui/tests/it/pointer.rs b/crates/ui-egui/tests/it/pointer.rs index 0a7dc80c63..e0021aa7be 100644 --- a/crates/ui-egui/tests/it/pointer.rs +++ b/crates/ui-egui/tests/it/pointer.rs @@ -1934,6 +1934,33 @@ fn a_right_click_with_the_button_held_opens_the_selected_comments_menu() { h.run_steps(2); } +/// With the page gaps hidden, a Shift-click on the row two pages share is the first page's: what it +/// selects there stays selected, and the second page leaves it alone. +#[test] +fn a_shift_click_where_two_pages_touch_keeps_what_it_selected() { + let mut h = open(bottom_line_pages_pdf(), "100", false); + h.state_mut().views[0].show_page_gaps = false; + assert!(h.state_mut().execute("edit.edit_text")); + h.run_steps(4); + let (one, two) = { + let v = &h.state().views[0]; + (v.page_screen_rect(0).expect("page 1 on screen"), v.page_screen_rect(1).expect("page 2 on screen")) + }; + // "Bottom line" runs from x 40, a few points above page 1's foot. + let seam = egui::pos2(one.min.x + 70.0, one.max.y); + assert!(one.contains(seam) && two.contains(seam), "the pages touch: {one:?} {two:?}"); + h.hover_at(seam); + h.run_steps(2); + for pressed in [true, false] { + h.event_modifiers( + egui::Event::PointerButton { pos: seam, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::SHIFT }, + egui::Modifiers::SHIFT, + ); + } + h.run_steps(4); + assert!(h.query_by_label_contains("1 object").is_some(), "the line is selected"); +} + /// With Prepare open on a document that can't be changed, a right-click on a field selects nothing /// and opens the page's menu, whose field entries would only be refused. #[test]