mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-05 01:59:30 +00:00
Fixes As Requested [Arranging works,Cropping fix, Small size issue,fonts,image override]
This commit is contained in:
1 parent
9a5fec0caa
commit
bf4c238eba
6 files changed
+233
-86
No files matched your search
+64
-31
@@ -603,33 +603,72 @@ pub fn arrange_image(doc: &mut Document, page: usize, index: usize, order: Arran
|
||||
};
|
||||
|
||||
let Some(target_unit) = units.get(target_idx) else { return Ok(()) };
|
||||
let (scope_start, scope_end, placement) = image_transform_scope(&ops, target)?.unwrap_or((target, target, Matrix(img.draw_matrix)));
|
||||
let has_other_paint = ops.iter().enumerate().any(|(i, op)| {
|
||||
i >= target_unit.start
|
||||
&& i < target_unit.end
|
||||
&& i != target
|
||||
&& matches!(
|
||||
op.op.as_slice(),
|
||||
b"Do" | b"Tj" | b"TJ" | b"'" | b"\"" | b"sh" | b"BI" | b"S" | b"s" | b"f" | b"F" | b"f*" | b"B" | b"B*" | b"b" | b"b*"
|
||||
)
|
||||
});
|
||||
|
||||
let unit_ops = if scope_start != scope_end && target_unit.start == scope_start && target_unit.end == scope_end.saturating_add(1) {
|
||||
if placement == Matrix::IDENTITY {
|
||||
ops.get(scope_start..=scope_end).unwrap_or_default().to_vec()
|
||||
let (target_span, unit_ops) = if has_other_paint {
|
||||
if let Ok(Some((sub_start, sub_end, sub_placement))) = image_exclusive_scope(&ops, target)
|
||||
&& sub_start >= target_unit.start
|
||||
&& sub_end < target_unit.end
|
||||
{
|
||||
let u_ops = if sub_placement == Matrix::IDENTITY {
|
||||
ops.get(sub_start..=sub_end).unwrap_or_default().to_vec()
|
||||
} else {
|
||||
let mut out = Vec::new();
|
||||
out.push(Op::new("q", vec![]));
|
||||
out.push(Op::new("cm", sub_placement.0.map(pdfcraft_content::num).to_vec()));
|
||||
if let Some(inner) = ops.get(sub_start.saturating_add(1)..=sub_end) {
|
||||
out.extend_from_slice(inner);
|
||||
}
|
||||
out
|
||||
};
|
||||
let start_op = ops.get(sub_start).unwrap_or(&ops[target]);
|
||||
let end_op = ops.get(sub_end).unwrap_or(&ops[target]);
|
||||
(start_op.span.start..end_op.span.end, u_ops)
|
||||
} else {
|
||||
let mut out = Vec::new();
|
||||
out.push(Op::new("q", vec![]));
|
||||
out.push(Op::new("cm", placement.0.map(pdfcraft_content::num).to_vec()));
|
||||
if let Some(inner) = ops.get(scope_start.saturating_add(1)..=scope_end) {
|
||||
out.extend_from_slice(inner);
|
||||
}
|
||||
out
|
||||
let u_ops = vec![
|
||||
Op::new("q", vec![]),
|
||||
Op::new("cm", img.draw_matrix.map(pdfcraft_content::num).to_vec()),
|
||||
ops.get(target).cloned().unwrap_or_else(|| Op::new("Do", vec![Object::name(&img.name)])),
|
||||
Op::new("Q", vec![]),
|
||||
];
|
||||
let target_op = &ops[target];
|
||||
(target_op.span.start..target_op.span.end, u_ops)
|
||||
}
|
||||
} else {
|
||||
vec![
|
||||
Op::new("q", vec![]),
|
||||
Op::new("cm", img.draw_matrix.map(pdfcraft_content::num).to_vec()),
|
||||
ops.get(target).cloned().unwrap_or_else(|| Op::new("Do", vec![Object::name(&img.name)])),
|
||||
Op::new("Q", vec![]),
|
||||
]
|
||||
let (scope_start, scope_end, placement) = image_transform_scope(&ops, target)?.unwrap_or((target, target, Matrix(img.draw_matrix)));
|
||||
let u_ops = if scope_start != scope_end && target_unit.start == scope_start && target_unit.end == scope_end.saturating_add(1) {
|
||||
if placement == Matrix::IDENTITY {
|
||||
ops.get(scope_start..=scope_end).unwrap_or_default().to_vec()
|
||||
} else {
|
||||
let mut out = Vec::new();
|
||||
out.push(Op::new("q", vec![]));
|
||||
out.push(Op::new("cm", placement.0.map(pdfcraft_content::num).to_vec()));
|
||||
if let Some(inner) = ops.get(scope_start.saturating_add(1)..=scope_end) {
|
||||
out.extend_from_slice(inner);
|
||||
}
|
||||
out
|
||||
}
|
||||
} else {
|
||||
vec![
|
||||
Op::new("q", vec![]),
|
||||
Op::new("cm", img.draw_matrix.map(pdfcraft_content::num).to_vec()),
|
||||
ops.get(target).cloned().unwrap_or_else(|| Op::new("Do", vec![Object::name(&img.name)])),
|
||||
Op::new("Q", vec![]),
|
||||
]
|
||||
};
|
||||
let Some(first_op) = ops.get(target_unit.start) else { return Ok(()) };
|
||||
let Some(last_op) = target_unit.end.checked_sub(1).and_then(|i| ops.get(i)) else { return Ok(()) };
|
||||
(first_op.span.start..last_op.span.end, u_ops)
|
||||
};
|
||||
|
||||
let Some(first_op) = ops.get(target_unit.start) else { return Ok(()) };
|
||||
let Some(last_op) = target_unit.end.checked_sub(1).and_then(|i| ops.get(i)) else { return Ok(()) };
|
||||
let target_span = first_op.span.start..last_op.span.end;
|
||||
|
||||
let dest_pos = if insert_after {
|
||||
let Some(op) = dest_unit.end.checked_sub(1).and_then(|i| ops.get(i)) else { return Ok(()) };
|
||||
op.span.end
|
||||
@@ -851,21 +890,15 @@ pub fn crop_image(doc: &mut Document, page: usize, index: usize, crop_rect: [f64
|
||||
(start, end, drawing_ops, scope_matrix)
|
||||
} else {
|
||||
let mut state = ImageState::new(Matrix::IDENTITY, p.crop(doc), Vec::new());
|
||||
let mut scope_matrix = Matrix::IDENTITY;
|
||||
let start = if target >= 1 && ops.get(target - 1).is_some_and(|o| o.is("cm")) {
|
||||
target - 1
|
||||
} else {
|
||||
target
|
||||
};
|
||||
for (i, o) in ops.iter().enumerate() {
|
||||
if i == start {
|
||||
scope_matrix = state.placement.matrix;
|
||||
if i == target {
|
||||
break;
|
||||
}
|
||||
state.apply(o)?;
|
||||
}
|
||||
let drawing_ops = ops.get(start..=target).unwrap_or_default().to_vec();
|
||||
(start, target, drawing_ops, scope_matrix)
|
||||
let scope_matrix = state.placement.matrix;
|
||||
let drawing_ops = vec![ops.get(target).cloned().unwrap_or_else(|| Op::new("Do", vec![Object::name(&img.name)]))];
|
||||
(target, target, drawing_ops, scope_matrix)
|
||||
};
|
||||
|
||||
let mut unit_ops = Vec::new();
|
||||
|
||||
@@ -1768,6 +1768,43 @@ fn image_arrange_stacking_order_and_remove_crop() {
|
||||
assert_eq!(img_restored_moved.visible_rect, Some(img_restored_moved.rect));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_arrange_preserves_shared_group_drawings() {
|
||||
let content = "q 100 0 0 100 100 100 cm /Im0 Do /Im0 Do Q BT /F1 12 Tf 72 700 Td (End Text) Tj ET";
|
||||
let mut doc = image_page_with(content);
|
||||
let initial = images::page_images(&doc, 0).unwrap();
|
||||
assert_eq!(initial.len(), 2, "both images initially present in shared group");
|
||||
|
||||
// Bring image 0 to front: it must not delete image 1 that shares the same q...Q group!
|
||||
images::arrange_image(&mut doc, 0, 0, images::ArrangeOrder::BringToFront).unwrap();
|
||||
let doc = reopen(&doc);
|
||||
let after = images::page_images(&doc, 0).unwrap();
|
||||
assert_eq!(after.len(), 2, "both images must be preserved after BringToFront");
|
||||
let bytes = page_content_bytes(&doc, 0);
|
||||
let str_content = String::from_utf8_lossy(&bytes);
|
||||
assert!(str_content.contains("End Text"), "text in page must be preserved");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_crop_preserves_outer_cm_for_subsequent_drawings() {
|
||||
let content = "100 0 0 100 50 50 cm /Im0 Do /Im0 Do";
|
||||
let mut doc = image_page_with(content);
|
||||
let initial = images::page_images(&doc, 0).unwrap();
|
||||
assert_eq!(initial.len(), 2);
|
||||
assert_eq!(initial[0].rect, [50.0, 50.0, 150.0, 150.0]);
|
||||
assert_eq!(initial[1].rect, [50.0, 50.0, 150.0, 150.0]);
|
||||
|
||||
// Crop image 0
|
||||
images::crop_image(&mut doc, 0, 0, [60.0, 60.0, 120.0, 120.0]).unwrap();
|
||||
let doc = reopen(&doc);
|
||||
let after = images::page_images(&doc, 0).unwrap();
|
||||
assert_eq!(after.len(), 2, "both images must remain");
|
||||
// Image 0 is cropped:
|
||||
assert!(close(after[0].visible_rect.unwrap(), [60.0, 60.0, 120.0, 120.0]));
|
||||
// Crucial: Image 1 must still have its rect at [50, 50, 150, 150], proving outer cm was not destroyed!
|
||||
assert_eq!(after[1].rect, [50.0, 50.0, 150.0, 150.0], "subsequent image must retain global cm");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn form_artwork_edits_only_the_selected_placement() {
|
||||
|
||||
@@ -560,25 +560,6 @@ pub(crate) fn format_panel(ui: &mut egui::Ui, t: &Tokens, style: &AddedText) ->
|
||||
for f in [FontFamily::Helvetica, FontFamily::Times, FontFamily::Courier] {
|
||||
ui.selectable_value(&mut s.family, f, f.label());
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(installed) = crate::files::installed_font_files(ui.ctx())
|
||||
&& !installed.is_empty()
|
||||
{
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new(tl!("Installed fonts")).weak().small());
|
||||
for font in &installed {
|
||||
let fam = if font.family.eq_ignore_ascii_case("times") || font.face.to_ascii_lowercase().contains("times") {
|
||||
FontFamily::Times
|
||||
} else if font.family.eq_ignore_ascii_case("courier") || font.face.to_ascii_lowercase().contains("courier") {
|
||||
FontFamily::Courier
|
||||
} else {
|
||||
FontFamily::Helvetica
|
||||
};
|
||||
if ui.selectable_label(s.family == fam, &font.face).clicked() {
|
||||
s.family = fam;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
egui::ComboBox::from_id_salt("font-size")
|
||||
@@ -789,18 +770,16 @@ impl crate::PdfCraftApp {
|
||||
|
||||
/// Whether there is an image in the in-memory clipboard or system clipboard ready to paste.
|
||||
pub fn can_paste_image(&self) -> bool {
|
||||
if self.last_copied_image.is_some() || self.last_copied_file.is_some() {
|
||||
return true;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
if self.system_clipboard
|
||||
&& let Ok(mut cb) = arboard::Clipboard::new()
|
||||
&& cb.get_image().is_ok()
|
||||
{
|
||||
return cb.get_image().is_ok();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
self.last_copied_image.is_some() || self.last_copied_file.is_some()
|
||||
}
|
||||
|
||||
/// Paste an image from the clipboard onto `page`.
|
||||
@@ -813,27 +792,40 @@ impl crate::PdfCraftApp {
|
||||
return false;
|
||||
}
|
||||
|
||||
let image_data: Option<(String, Vec<u8>)> = if let Some((name, bytes)) = self.last_copied_file.clone() {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let os_png = if self.system_clipboard {
|
||||
if let Ok(mut cb) = arboard::Clipboard::new()
|
||||
&& let Ok(img) = cb.get_image()
|
||||
{
|
||||
let matches_internal = self.last_copied_image.as_ref().is_some_and(|(w, h, rgba)| {
|
||||
*w as usize == img.width && *h as usize == img.height && rgba.as_slice() == img.bytes.as_ref()
|
||||
});
|
||||
if !matches_internal {
|
||||
crate::create_ui::png_from_rgba(img.width, img.height, &img.bytes).ok().map(|bytes| {
|
||||
(img.width as u32, img.height as u32, img.bytes.to_vec(), bytes)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let os_png: Option<(u32, u32, Vec<u8>, Vec<u8>)> = None;
|
||||
|
||||
let image_data: Option<(String, Vec<u8>)> = if let Some((w, h, rgba, png_bytes)) = os_png {
|
||||
self.last_copied_image = Some((w, h, rgba));
|
||||
self.last_copied_file = Some(("clipboard_image.png".to_string(), png_bytes.clone()));
|
||||
self.last_copied_image_rect = None;
|
||||
Some(("clipboard_image.png".to_string(), png_bytes))
|
||||
} else if let Some((name, bytes)) = self.last_copied_file.clone() {
|
||||
Some((name, bytes))
|
||||
} else if let Some((w, h, rgba)) = self.last_copied_image.clone() {
|
||||
crate::create_ui::png_from_rgba(w as usize, h as usize, &rgba).ok().map(|b| ("pasted_image.png".to_string(), b))
|
||||
} else {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
if self.system_clipboard {
|
||||
arboard::Clipboard::new()
|
||||
.ok()
|
||||
.and_then(|mut cb| cb.get_image().ok())
|
||||
.and_then(|img| {
|
||||
crate::create_ui::png_from_rgba(img.width, img.height, &img.bytes)
|
||||
.ok()
|
||||
.map(|b| ("clipboard_image.png".to_string(), b))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
None
|
||||
};
|
||||
|
||||
|
||||
@@ -975,41 +975,73 @@ pub(crate) fn image_input(
|
||||
let initial = drag.initial_rect;
|
||||
if is_crop {
|
||||
let mut new_crop = initial;
|
||||
let img_screen_w = b.width().max(0.0);
|
||||
let img_screen_h = b.height().max(0.0);
|
||||
let safe_min_w = 10.0_f32.min(img_screen_w).min(initial.width().max(0.0)).max(0.0);
|
||||
let safe_min_h = 10.0_f32.min(img_screen_h).min(initial.height().max(0.0)).max(0.0);
|
||||
match drag.target {
|
||||
DragTarget::CropHandle(ImageHandle::TopLeft) => {
|
||||
let x = (initial.left() + delta.x).clamp(b.left(), initial.right() - 10.0);
|
||||
let y = (initial.top() + delta.y).clamp(b.top(), initial.bottom() - 10.0);
|
||||
let min_x = b.left();
|
||||
let max_x = (initial.right() - safe_min_w).max(min_x);
|
||||
let x = (initial.left() + delta.x).clamp(min_x, max_x);
|
||||
|
||||
let min_y = b.top();
|
||||
let max_y = (initial.bottom() - safe_min_h).max(min_y);
|
||||
let y = (initial.top() + delta.y).clamp(min_y, max_y);
|
||||
new_crop = Rect::from_min_max(Pos2::new(x, y), initial.max);
|
||||
}
|
||||
DragTarget::CropHandle(ImageHandle::TopRight) => {
|
||||
let x = (initial.right() + delta.x).clamp(initial.left() + 10.0, b.right());
|
||||
let y = (initial.top() + delta.y).clamp(b.top(), initial.bottom() - 10.0);
|
||||
let min_x = (initial.left() + safe_min_w).min(b.right());
|
||||
let max_x = b.right().max(min_x);
|
||||
let x = (initial.right() + delta.x).clamp(min_x, max_x);
|
||||
|
||||
let min_y = b.top();
|
||||
let max_y = (initial.bottom() - safe_min_h).max(min_y);
|
||||
let y = (initial.top() + delta.y).clamp(min_y, max_y);
|
||||
new_crop = Rect::from_min_max(Pos2::new(initial.left(), y), Pos2::new(x, initial.bottom()));
|
||||
}
|
||||
DragTarget::CropHandle(ImageHandle::BottomLeft) => {
|
||||
let x = (initial.left() + delta.x).clamp(b.left(), initial.right() - 10.0);
|
||||
let y = (initial.bottom() + delta.y).clamp(initial.top() + 10.0, b.bottom());
|
||||
let min_x = b.left();
|
||||
let max_x = (initial.right() - safe_min_w).max(min_x);
|
||||
let x = (initial.left() + delta.x).clamp(min_x, max_x);
|
||||
|
||||
let min_y = (initial.top() + safe_min_h).min(b.bottom());
|
||||
let max_y = b.bottom().max(min_y);
|
||||
let y = (initial.bottom() + delta.y).clamp(min_y, max_y);
|
||||
new_crop = Rect::from_min_max(Pos2::new(x, initial.top()), Pos2::new(initial.right(), y));
|
||||
}
|
||||
DragTarget::CropHandle(ImageHandle::BottomRight) => {
|
||||
let x = (initial.right() + delta.x).clamp(initial.left() + 10.0, b.right());
|
||||
let y = (initial.bottom() + delta.y).clamp(initial.top() + 10.0, b.bottom());
|
||||
let min_x = (initial.left() + safe_min_w).min(b.right());
|
||||
let max_x = b.right().max(min_x);
|
||||
let x = (initial.right() + delta.x).clamp(min_x, max_x);
|
||||
|
||||
let min_y = (initial.top() + safe_min_h).min(b.bottom());
|
||||
let max_y = b.bottom().max(min_y);
|
||||
let y = (initial.bottom() + delta.y).clamp(min_y, max_y);
|
||||
new_crop = Rect::from_min_max(initial.min, Pos2::new(x, y));
|
||||
}
|
||||
DragTarget::CropHandle(ImageHandle::TopCenter) => {
|
||||
let y = (initial.top() + delta.y).clamp(b.top(), initial.bottom() - 10.0);
|
||||
let min_y = b.top();
|
||||
let max_y = (initial.bottom() - safe_min_h).max(min_y);
|
||||
let y = (initial.top() + delta.y).clamp(min_y, max_y);
|
||||
new_crop = Rect::from_min_max(Pos2::new(initial.left(), y), initial.max);
|
||||
}
|
||||
DragTarget::CropHandle(ImageHandle::BottomCenter) => {
|
||||
let y = (initial.bottom() + delta.y).clamp(initial.top() + 10.0, b.bottom());
|
||||
let min_y = (initial.top() + safe_min_h).min(b.bottom());
|
||||
let max_y = b.bottom().max(min_y);
|
||||
let y = (initial.bottom() + delta.y).clamp(min_y, max_y);
|
||||
new_crop = Rect::from_min_max(initial.min, Pos2::new(initial.right(), y));
|
||||
}
|
||||
DragTarget::CropHandle(ImageHandle::LeftCenter) => {
|
||||
let x = (initial.left() + delta.x).clamp(b.left(), initial.right() - 10.0);
|
||||
let min_x = b.left();
|
||||
let max_x = (initial.right() - safe_min_w).max(min_x);
|
||||
let x = (initial.left() + delta.x).clamp(min_x, max_x);
|
||||
new_crop = Rect::from_min_max(Pos2::new(x, initial.top()), initial.max);
|
||||
}
|
||||
DragTarget::CropHandle(ImageHandle::RightCenter) => {
|
||||
let x = (initial.right() + delta.x).clamp(initial.left() + 10.0, b.right());
|
||||
let min_x = (initial.left() + safe_min_w).min(b.right());
|
||||
let max_x = b.right().max(min_x);
|
||||
let x = (initial.right() + delta.x).clamp(min_x, max_x);
|
||||
new_crop = Rect::from_min_max(initial.min, Pos2::new(x, initial.bottom()));
|
||||
}
|
||||
DragTarget::Move => {
|
||||
|
||||
@@ -73,7 +73,8 @@ mod autoscroll;
|
||||
mod bidi;
|
||||
pub mod bulk_fields;
|
||||
mod dialogs;
|
||||
mod edit_text_ui;
|
||||
pub mod edit_text_ui;
|
||||
pub use edit_text_ui::{DragTarget, ImageDrag, ImageHandle, ImageSelection};
|
||||
mod editing;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod file_watch;
|
||||
|
||||
@@ -2717,11 +2717,63 @@ fn image_edge_handle_stretch_and_crop_mode() {
|
||||
h.event(egui::Event::PointerButton { pos: c, button: egui::PointerButton::Secondary, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Remove crop").click();
|
||||
h.run_steps(4);
|
||||
let images = h.state().session.get(doc_id).unwrap().page_images(0);
|
||||
assert_eq!(images[0].visible_rect, Some(images[0].rect), "crop is removed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_image_crop_handle_drag_does_not_panic() {
|
||||
let mut png = Vec::new();
|
||||
image::RgbImage::from_pixel(5, 5, image::Rgb([100, 150, 200])).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(800.0, 600.0)).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("tiny.png", None, png.clone()).expect("opens");
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
assert!(h.state_mut().execute("edit.edit_text"));
|
||||
h.run_steps(2);
|
||||
|
||||
// Enter crop mode on image 0
|
||||
let view = &mut h.state_mut().views[0];
|
||||
view.image_selection = Some(pdfcraft_ui_egui::ImageSelection {
|
||||
page: 0,
|
||||
index: 0,
|
||||
drag: None,
|
||||
crop_mode: true,
|
||||
crop_rect: None,
|
||||
});
|
||||
h.run_steps(1);
|
||||
|
||||
// Simulate drag on TopLeft crop handle with an initial_rect of 5.0 x 5.0
|
||||
let initial = egui::Rect::from_min_size(egui::pos2(100.0, 100.0), egui::vec2(5.0, 5.0));
|
||||
let view = &mut h.state_mut().views[0];
|
||||
if let Some(s) = view.image_selection.as_mut() {
|
||||
s.drag = Some(pdfcraft_ui_egui::ImageDrag {
|
||||
start: egui::pos2(100.0, 100.0),
|
||||
target: pdfcraft_ui_egui::DragTarget::CropHandle(pdfcraft_ui_egui::ImageHandle::TopLeft),
|
||||
initial_rect: initial,
|
||||
});
|
||||
}
|
||||
|
||||
// Hover/drag to a new position, triggering the clamp calculation
|
||||
h.hover_at(egui::pos2(105.0, 105.0));
|
||||
h.run_steps(2);
|
||||
|
||||
// Also test BottomRight handle
|
||||
let view = &mut h.state_mut().views[0];
|
||||
if let Some(s) = view.image_selection.as_mut() {
|
||||
s.drag = Some(pdfcraft_ui_egui::ImageDrag {
|
||||
start: egui::pos2(105.0, 105.0),
|
||||
target: pdfcraft_ui_egui::DragTarget::CropHandle(pdfcraft_ui_egui::ImageHandle::BottomRight),
|
||||
initial_rect: initial,
|
||||
});
|
||||
}
|
||||
h.hover_at(egui::pos2(95.0, 95.0));
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_selection_action_icons() {
|
||||
let mut h = harness(1, |_| {});
|
||||
|
||||
Reference in new issue
Block a user