Merge PR #3879: M7: an added picture resized by a corner shows only at its new size

Conflict resolutions:
- crates/ui-egui/tests/content.rs: both test sets appended at the file end kept (main's format-text test, then #3879's picture-resize helpers and test).
This commit is contained in:
aiongg
2026-10-22 01:50:45 +00:00
2 changed files with 98 additions and 9 deletions
+23 -9
View File
@@ -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<u64>,
/// 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);
+75
View File
@@ -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");
}