Compare commits

...
3 changed files with 107 additions and 6 deletions
+94 -1
View File
@@ -2626,7 +2626,10 @@ fn drag_floating_picture(wrap: &str, header: bool, preselect: bool) {
for _ in 0..3 {
h.step();
}
assert!(wordcraft_engine::cmd::objects::object_selection(&h.state().session).is_none(), "a click still selects text");
// In the header the picture lies over the body area, clear of the header's own text, so a
// click on it selects it (Word); in the body a click still reaches the text behind it.
let picked = wordcraft_engine::cmd::objects::object_selection(&h.state().session).is_some();
assert_eq!(picked, header, "a click selects the header's picture, but only edits the body's text");
if preselect {
let end = Pos { off: pos.off + wordcraft_doc::para::OBJ.len_utf8(), ..pos.clone() };
h.state_mut().run("select.range", json!({"anchor": pos, "focus": end})).unwrap();
@@ -4934,6 +4937,96 @@ fn a_long_comment_balloon_paints_only_its_first_ten_rows() {
}
}
/// A Behind Text header picture: while editing the header a click on it selects it wherever no
/// header text covers it (far down over the body, or in the band clear of the text), as in Word;
/// over the header text the click edits the text. From the body a click there is body text.
#[test]
fn header_picture_clear_of_header_text_is_selected_while_editing_the_header() {
// Far down the page, over the body.
header_picture_click(200.0, 400.0, Some((300.0, f32::MAX)), true);
// In the header band, below the header's text line.
header_picture_click(300.0, 56.0, Some((50.0, 72.0)), true);
// Over the header's text, at its start: the click goes to the text.
header_picture_click(72.0, 36.0, None, false);
}
/// Puts a Behind Text header picture at (`x`, `y`), checks its top lies within `band` (when
/// given), clicks near its top-left corner from the body and then while editing the header, and checks the
/// second click selects it (`selects`) or leaves the caret in the header's text.
fn header_picture_click(x: f32, y: f32, band: Option<(f32, f32)>, selects: bool) {
use egui::{Event, PointerButton};
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default());
a.run("insert.editHeader", json!({})).unwrap();
a.run("text.insert", json!({"text": "Header"})).unwrap();
let mut png = Vec::new();
let img = image::RgbaImage::from_pixel(40, 20, image::Rgba([200, 30, 30, 255]));
image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
a.run("insert.picture", json!({"data": wordcraft_engine::cmd::insert::base64_encode(&png), "width": 160})).unwrap();
let pos = wordcraft_engine::cmd::objects::selected(&a.session).unwrap().0;
a.run("arrange.bounds", json!({"x": x, "y": y})).unwrap();
a.run("arrange.wrap", json!({"wrap": "behindText"})).unwrap();
let rect = a.session.layout().object(&pos, 0).unwrap().rect;
if let Some((lo, hi)) = band {
assert!(rect.y >= lo && rect.y < hi, "the picture sits where the case wants it: {rect:?}");
}
a.run("insert.closeHeader", json!({})).unwrap();
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).with_step_dt(1.0 / 60.0).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
a,
);
for _ in 0..6 {
h.step();
}
let page = h.state().canvas.page_rects[0];
let scale = h.state().canvas.scale;
let frame = crate::frame::to_screen(page, scale, rect);
// Near its top-left corner: on the header text line when the picture is placed over it.
let at = frame.min + egui::vec2(10.0, 6.0) * scale;
let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
let click = |h: &mut egui_kittest::Harness<'_, WordApp>| {
h.event(Event::PointerMoved(at));
h.event(button(true));
h.step();
h.event(button(false));
for _ in 0..3 {
h.step();
}
};
click(&mut h);
assert!(wordcraft_engine::cmd::objects::object_selection(&h.state().session).is_none(), "from the body the header picture isn't selectable");
assert_eq!(h.state().session.sel.focus.story, StoryRef::Body);
h.state_mut().run("insert.editHeader", json!({"page": 0})).unwrap();
for _ in 0..6 {
h.step();
}
click(&mut h);
let picked = wordcraft_engine::cmd::objects::object_selection(&h.state().session).map(|(p, _)| p);
if selects {
assert_eq!(picked, Some(pos.clone()), "while editing the header a click on its picture selects it ({x}, {y})");
} else {
assert_eq!(picked, None, "over the header's text the click edits the text ({x}, {y})");
assert!(matches!(h.state().session.sel.focus.story, StoryRef::Part(_)), "still in the header");
}
// A right-click does the same, so the context menu (Reset Picture, Wrap Text) acts on it.
h.state_mut().session.sel = wordcraft_engine::Selection::caret(Pos { off: 0, ..pos.clone() });
let right = |pressed| Event::PointerButton { pos: at, button: PointerButton::Secondary, pressed, modifiers: Default::default() };
h.event(Event::PointerMoved(at));
h.event(right(true));
h.step();
h.event(right(false));
for _ in 0..3 {
h.step();
}
let picked = wordcraft_engine::cmd::objects::object_selection(&h.state().session).map(|(p, _)| p);
assert_eq!(picked, selects.then(|| pos.clone()), "a right-click picks the picture only where a click does ({x}, {y})");
h.event(Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() });
h.step();
}
/// #1961 review: what a picture draws must be in its page's fingerprint itself. Reset Picture on
/// one never edited keeps its media and clears only its corrections and crop, and undo puts them
/// back; the anchor paragraph's layout pointer also moves then, but only as long as the old
+11 -4
View File
@@ -289,7 +289,15 @@ pub fn pointer(app: &mut WordApp, ui: &Ui, resp: &egui::Response, pages: &[Rect]
let _ = app.run("select.addObject", json!({"pos": object.pos()}));
return true;
}
if pressed && grab == Grab::Move && object.behind() && behind_draggable(app, &object) {
// While editing a header/footer, a click on its Behind Text picture where no header/footer
// text covers it (the plain hit test picked it: down over the body, or clear of the text in
// the band) selects it, as in Word; only over text does the click defer to that text.
// ponytail: headers/footers only; body Behind Text pictures clear of text could do the same.
let clear_of_text = matches!(object.story, StoryRef::Part(_))
&& crate::canvas::page_at(pages, layout, scale, at)
.and_then(|(page, x, y)| layout.object_at_in(page, x, y, EDGE / scale.max(0.01), object.story))
.is_some_and(|o| o.pos() == object.pos());
if pressed && grab == Grab::Move && object.behind() && !clear_of_text && behind_draggable(app, &object) {
let (min, picture) = match app.session.doc.para_at(&object.pos()).and_then(|p| p.object_at(object.pos().off)) {
Some(o) => (wordcraft_engine::cmd::objects::min_size(o), matches!(o, InlineObject::Image { .. })),
None => return false,
@@ -441,9 +449,8 @@ fn link_click(app: &mut WordApp, layout: &DocLayout, pages: &[Rect], scale: f32,
/// Right-click at `at`: the picture, shape or text box under it becomes the selection (as a click
/// would, without a drag), so the context menu acts on it. Returns whether there was one.
pub fn select_at(app: &mut WordApp, layout: &DocLayout, pages: &[Rect], scale: f32, at: Pos2) -> bool {
if crate::canvas::editing_header_footer(app, layout) {
return false;
}
// While editing a header/footer, `grab_at` looks at its own objects, so its picture is picked
// too and the menu's Reset Picture or Wrap Text acts on it.
let Some((object, _)) = grab_at(app, layout, pages, scale, at) else { return false };
let pos = object.pos();
// Already selected (alone or among several picked for Group): keep the selection.
+2 -1
View File
File diff suppressed because one or more lines are too long