From 1f335a2bad3217048d8b78c751a81e6c5e242167 Mon Sep 17 00:00:00 2001 From: komo1iddin <45395275+komo1iddin@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:45:00 +0800 Subject: [PATCH] M7: an added picture resized by a corner shows only at its new size The page without an added item is rendered once per gesture so the item's pixels can follow a move (MoveBackground). A corner resize got none of that: only the outline followed the corner, the picture stayed at its old size under it for the whole drag, and showed there again after the release until the page was rendered. Give a picture's corner resize the same background, and stretch its pixels from the box it had into the dragged box, then into the applied box while the page settles. Text resized by a corner rewraps, so it keeps the outline only. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/content_ui.rs | 32 ++++++++++---- crates/ui-egui/tests/content.rs | 75 ++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/crates/ui-egui/src/content_ui.rs b/crates/ui-egui/src/content_ui.rs index 5f5ef32f7..fa1607fc6 100644 --- a/crates/ui-egui/src/content_ui.rs +++ b/crates/ui-egui/src/content_ui.rs @@ -109,8 +109,9 @@ impl ContentView { } } -/// While an added item is moved, the page without it, rendered once per gesture: the item's own -/// pixels follow the pointer over it instead of staying put until release (#1488). +/// While an added item is moved, or a picture resized by a corner, the page without it, rendered +/// once per gesture: the item's own pixels follow the pointer over it instead of staying put until +/// release (#1488). #[derive(Default)] pub(crate) struct MoveBackground { key: Option<(u64, usize, usize)>, @@ -119,6 +120,8 @@ pub(crate) struct MoveBackground { tag: Option, /// The item's display-space box when the gesture started. from: Option<[f64; 4]>, + /// The item is a picture: a corner stretches its pixels, and it keeps its whole applied box. + picture: bool, /// The applied move's box. The page raster on screen still has the item where it was until /// its replacement arrives, so the preview stays up until then. settling: Option<[f64; 4]>, @@ -130,8 +133,11 @@ impl MoveBackground { } pub fn prepare(&mut self, ctx: &egui::Context, doc: &pdfcraft_engine::Document, cv: &ContentView, scale: f32) { + let picture = |page, index| on_page(doc.added(), page).into_iter().any(|(i, a)| i == index && matches!(a.content, AddedContent::Image(_))); let key = match (cv.grab, cv.selected) { (Some((gp, Grab::Move, _)), Some((sp, si))) if gp == sp => Some((doc.edit_generation(), sp, si)), + // Text resized by a corner rewraps, so only a picture's pixels can follow the corner. + (Some((gp, Grab::Corner(..), _)), Some((sp, si))) if gp == sp && picture(sp, si) => Some((doc.edit_generation(), sp, si)), _ => None, }; if !(key.is_none() && self.settling.is_some()) && self.key != key { @@ -139,6 +145,7 @@ impl MoveBackground { if let Some((_, page, index)) = key { self.renderer = doc.added_content_preview(page, index).ok().map(|p| p.renderer()); self.from = on_page(doc.added(), page).into_iter().find(|(i, _)| *i == index).map(|(_, a)| a.content.rect()); + self.picture = picture(page, index); } } if self.settling.is_some() { @@ -208,22 +215,29 @@ impl MoveBackground { return; } let r = screen_rect(xf, info, page, from); - let d = if let Some((_, _, start)) = cv.grab { + let to = if let Some((_, grab, start)) = cv.grab { let Some(p) = ui.input(|i| i.pointer.hover_pos()) else { return }; - p - start + dragged(r, grab, p - start, self.picture) } else if let Some(to) = self.settling { + let to = screen_rect(xf, info, page, to); // A moved text box keeps its top edge (its height follows its lines). - screen_rect(xf, info, page, to).left_top() - r.left_top() + if self.picture { to } else { r.translate(to.left_top() - r.left_top()) } } else { return; }; let painter = painter.with_clip_rect(painter.clip_rect().intersect(xf.rect)); xf.paint_image(&painter, background.id(), 0.0, 0.0, 1.0, 1.0); - let r = r.expand(1.0); + // A pixel around the box, so its edges come along. + let (r, to) = (r.expand(1.0), to.expand(1.0)); let mut mesh = egui::Mesh::with_texture(page_tex); - for c in [r.left_top(), r.right_top(), r.right_bottom(), r.left_bottom()] { - let (u, v) = xf.screen_to_norm(c); - mesh.vertices.push(egui::epaint::Vertex { pos: c + d, uv: Pos2::new(u, v), color: Color32::WHITE }); + for (a, b) in [ + (r.left_top(), to.left_top()), + (r.right_top(), to.right_top()), + (r.right_bottom(), to.right_bottom()), + (r.left_bottom(), to.left_bottom()), + ] { + let (u, v) = xf.screen_to_norm(a); + mesh.vertices.push(egui::epaint::Vertex { pos: b, uv: Pos2::new(u, v), color: Color32::WHITE }); } mesh.add_triangle(0, 1, 2); mesh.add_triangle(0, 2, 3); diff --git a/crates/ui-egui/tests/content.rs b/crates/ui-egui/tests/content.rs index dd2c032ef..967578aa6 100644 --- a/crates/ui-egui/tests/content.rs +++ b/crates/ui-egui/tests/content.rs @@ -585,3 +585,78 @@ fn released_text_does_not_jump_back_before_the_page_is_redrawn() { let (old, new) = ink_at(&mut h, from, to); assert!(new > 20 && old == 0, "the redrawn page has the text in its new place ({old} old, {new} new)"); } + +/// Red pixels in `r`: the test picture. +fn red(pixels: &image::RgbaImage, r: egui::Rect) -> usize { + let (x0, y0, x1, y1) = (r.left().max(0.0) as u32, r.top().max(0.0) as u32, r.right() as u32, r.bottom() as u32); + (y0..y1.min(pixels.height())) + .flat_map(|y| (x0..x1.min(pixels.width())).map(move |x| (x, y))) + .filter(|(x, y)| matches!(pixels.get_pixel(*x, *y).0, [r, g, b, _] if r > 150 && g < 90 && b < 90)) + .count() +} + +/// Steps until no render is pending. +fn settle(h: &mut Harness<'static, PdfCraftApp>) { + for _ in 0..200 { + h.run_steps(1); + if !h.state().render_pending() { + return; + } + std::thread::sleep(std::time::Duration::from_millis(5)); + } + panic!("the page did not finish rendering"); +} + +/// An added picture resized by a corner is drawn at its new size while dragged and after the +/// release until the page is rendered again. It used to stay at its old size under the outline for +/// the whole drag, and to show there again after the release. +#[test] +fn a_resized_added_picture_shows_only_at_its_new_size() { + let mut h = harness(); + assert!(h.state_mut().execute("edit.edit_text")); + h.run_steps(2); + let mut png = Vec::new(); + { + let mut enc = png::Encoder::new(&mut png, 60, 60); + enc.set_color(png::ColorType::Rgb); + let mut w = enc.write_header().unwrap(); + w.write_image_data(&[200u8, 30, 30].repeat(3600)).unwrap(); + } + h.state_mut().add_image("red.png".into(), png); + h.run_steps(3); + assert_eq!(h.state().views[0].content.selected, Some((0, 0)), "the picture is selected"); + settle(&mut h); + let rect = added(&h)[0].content.rect(); + let r = item_on_screen(&h, rect); + // The lower right corner to the middle: half the size, the upper left corner kept. + let (grip, to) = (r.expand(1.0).right_bottom(), r.center()); + let kept = egui::Rect::from_min_max(r.min, r.center()).shrink(3.0); + let gone = egui::Rect::from_min_max(r.center() + egui::vec2(4.0, 4.0), r.max - egui::vec2(3.0, 3.0)); + h.hover_at(grip); + h.run_steps(1); + h.drag_at(grip); + h.run_steps(1); + for k in 1..=4 { + h.hover_at(grip + (to - grip) * (k as f32 / 4.0)); + h.run_steps(1); + } + settle(&mut h); + let pixels = h.render().unwrap(); + assert!(red(&pixels, kept) > 100, "while dragged, the picture is drawn at its new size"); + assert_eq!(red(&pixels, gone), 0, "while dragged, the picture shows at its old size"); + assert_eq!(added(&h)[0].content.rect(), rect, "nothing is applied before release"); + h.state_mut().views[0].hold_rasters = true; + h.drop_at(to); + for frame in 0..4 { + h.step(); + let pixels = h.render().unwrap(); + assert!(red(&pixels, kept) > 100, "frame {frame} after the release: the picture is not at its new size"); + assert_eq!(red(&pixels, gone), 0, "frame {frame} after the release: the picture shows at its old size"); + } + assert_ne!(added(&h)[0].content.rect(), rect, "the resize is applied"); + h.state_mut().views[0].hold_rasters = false; + settle(&mut h); + h.run_steps(2); + let pixels = h.render().unwrap(); + assert!(red(&pixels, kept) > 100 && red(&pixels, gone) == 0, "the page raster has the picture at its new size"); +}