diff --git a/crates/ui-egui/src/content_ui.rs b/crates/ui-egui/src/content_ui.rs index 87f4c7756..acfd039aa 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 70e5a0dcb..84e0f8459 100644 --- a/crates/ui-egui/tests/content.rs +++ b/crates/ui-egui/tests/content.rs @@ -642,3 +642,78 @@ fn format_text_panel_sets_alignment_and_lists_planned_controls_disabled() { h.run_steps(2); assert!(h.state().text_style.bold); } + +/// 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"); +}