mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-05 01:59:30 +00:00
Merge Conflicts #4889
This commit is contained in:
1 parent
2650cbc906
commit
5cf09552ab
16 files changed
+6207
-6159
No files matched your search
@@ -385,6 +385,11 @@ impl Automation {
|
||||
"flip_horizontal" => ImageEdit::Flip { horizontal: true },
|
||||
"flip_vertical" => ImageEdit::Flip { horizontal: false },
|
||||
"delete" => ImageEdit::Delete,
|
||||
"bring_to_front" => ImageEdit::Arrange(pdfcraft_engine::ArrangeOrder::BringToFront),
|
||||
"send_to_back" => ImageEdit::Arrange(pdfcraft_engine::ArrangeOrder::SendToBack),
|
||||
"bring_forward" => ImageEdit::Arrange(pdfcraft_engine::ArrangeOrder::BringForward),
|
||||
"send_backward" => ImageEdit::Arrange(pdfcraft_engine::ArrangeOrder::SendBackward),
|
||||
"remove_crop" => ImageEdit::RemoveCrop,
|
||||
"replace" => {
|
||||
let path = self.resolve(a.str("path")?, false)?;
|
||||
let bytes = read_input(&path)?;
|
||||
|
||||
@@ -530,7 +530,8 @@ pub(crate) fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut E
|
||||
},
|
||||
};
|
||||
|
||||
let added_items: Vec<(usize, pdfcraft_edit::Added)> = pdfcraft_edit::list_added(doc).into_iter().filter(|a| a.page == *page).enumerate().collect();
|
||||
let added_items: Vec<(usize, pdfcraft_edit::Added)> =
|
||||
pdfcraft_edit::list_added(doc).into_iter().filter(|a| a.page == *page).enumerate().collect();
|
||||
let matched_added = if let Some(st_ref) = target_stream_obj {
|
||||
added_items.iter().find(|(_, a)| a.obj == st_ref)
|
||||
} else if let Some(target_ref) = img.object {
|
||||
|
||||
@@ -326,7 +326,7 @@ impl PdfCraftApp {
|
||||
if !self.is_tool_pinned(id) {
|
||||
self.pinned_tools.push(id.to_string());
|
||||
if let Some(g) = pdfcraft_engine::catalog::group(id) {
|
||||
self.notify_fmt("Pinned {tool} to Home", &[("tool", &g.label)]);
|
||||
self.notify_fmt("Pinned {tool} to Home", &[("tool", g.label)]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -338,7 +338,7 @@ impl PdfCraftApp {
|
||||
if let Some(pos) = self.pinned_tools.iter().position(|t| t == id) {
|
||||
self.pinned_tools.remove(pos);
|
||||
if let Some(g) = pdfcraft_engine::catalog::group(id) {
|
||||
self.notify_fmt("Unpinned {tool} from Home", &[("tool", &g.label)]);
|
||||
self.notify_fmt("Unpinned {tool} from Home", &[("tool", g.label)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,16 +22,6 @@ impl PdfCraftApp {
|
||||
false
|
||||
}
|
||||
|
||||
/// ⌘C copies the selected text, unless a text field has the keyboard (it copies its own).
|
||||
pub(super) fn copy_key(&mut self, i: usize, ctx: &egui::Context) {
|
||||
// ⌘C arrives as a Copy event on most platforms.
|
||||
let copy = ctx.input(|input| input.events.iter().any(|e| matches!(e, egui::Event::Copy)));
|
||||
if !copy || ctx.egui_wants_keyboard_input() {
|
||||
return;
|
||||
}
|
||||
self.copy_selected_text(i, ctx);
|
||||
}
|
||||
|
||||
/// ⌘C / ⌘X / ⌘V copy, cut and paste text or images on canvas (unless a text field has focus).
|
||||
pub(super) fn clipboard_keys(&mut self, i: usize, ctx: &egui::Context, pasted: bool) {
|
||||
use egui::{Key, KeyboardShortcut, Modifiers};
|
||||
|
||||
+3419
-3419
File diff suppressed because it is too large.
Load diff
@@ -236,6 +236,12 @@ impl DocView {
|
||||
self.texts.get(&page).cloned()
|
||||
}
|
||||
|
||||
/// Whether `page`'s text layer has arrived from the renderer. Selecting text, the I-beam and
|
||||
/// find matches on a page all wait for it (tests and automation).
|
||||
pub fn has_page_text(&self, page: usize) -> bool {
|
||||
self.texts.contains_key(&page)
|
||||
}
|
||||
|
||||
pub fn thumb(&self, page: usize) -> Option<&TextureHandle> {
|
||||
self.thumbs.get(&page).map(|t| &t.tex)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ impl DocView {
|
||||
}
|
||||
|
||||
/// Where `page` is drawn this frame, with its coordinate mapping.
|
||||
pub(crate) fn page_xform(&self, page: usize) -> Option<PageXform> {
|
||||
pub fn page_xform(&self, page: usize) -> Option<PageXform> {
|
||||
self.screen_xforms.iter().find(|(p, _)| *p == page).map(|(_, xf)| *xf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,6 +694,7 @@ fn re_resolve_image_selection(
|
||||
.filter(|(_, img)| img.name == target.name && img.object == target.object && rect_matches(img.rect, target.rect))
|
||||
.map(|(idx, _)| idx)
|
||||
.collect();
|
||||
eprintln!("re_resolve: view.image_sel={:?}, target_rect={:?}, exact_matches={:?}, ambiguous_before={ambiguous_before}", view.image_selection.as_ref().map(|s| s.index), target.rect, exact_matches);
|
||||
let new_idx = if !ambiguous_before && exact_matches.len() == 1 {
|
||||
exact_matches.first().copied()
|
||||
} else if !ambiguous_before && exact_matches.is_empty() {
|
||||
|
||||
@@ -28,9 +28,9 @@ macro_rules! tl_ctx {
|
||||
pub mod canvas;
|
||||
mod chrome;
|
||||
pub mod image_engine;
|
||||
pub use image_engine::{HandleId, MIN_RECT_SIZE, apply_crop, apply_resize, apply_resize_proportional, reset_crop};
|
||||
pub use combine_grid::ThumbState as CombineThumb;
|
||||
pub use combine_ui::{Columns as CombineColumns, CombineView, Lock as CombineLock, SortKey};
|
||||
pub use image_engine::{HandleId, MIN_RECT_SIZE, apply_crop, apply_resize, apply_resize_proportional, reset_crop};
|
||||
mod commands;
|
||||
pub mod control;
|
||||
mod menus;
|
||||
@@ -140,8 +140,8 @@ pub use tools::bulk_fields;
|
||||
use tools::calc_order_ui;
|
||||
use tools::camera_ui;
|
||||
pub use tools::comments;
|
||||
use tools::content_ui;
|
||||
use tools::edit_text_ui;
|
||||
pub use tools::content_ui;
|
||||
pub use tools::edit_text_ui;
|
||||
pub use tools::field_table_ui;
|
||||
pub use tools::fill_sign;
|
||||
pub use tools::forms_ui;
|
||||
|
||||
@@ -254,6 +254,9 @@ fn english(labeled: Labeled<'_>) -> Option<String> {
|
||||
}
|
||||
|
||||
fn translate(english: &'static str) -> String {
|
||||
if english == "⋯" {
|
||||
return "…".to_owned();
|
||||
}
|
||||
tl!(english).to_owned()
|
||||
}
|
||||
|
||||
|
||||
@@ -264,6 +264,7 @@ fn format_section(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
.map(|f| (f.resource, f.face))
|
||||
.collect();
|
||||
let fmt = ed.selection_formatting(&sources);
|
||||
ed.sync_selection_look(&fmt);
|
||||
let shown = fmt.font_shown;
|
||||
let mixed_size = fmt.size.is_none();
|
||||
let out = crate::format_text_ui::panel(ui, t, &mut ed.look, Some(&mut ed.extras), Fonts { sources: Some(&sources), shown, mixed_size });
|
||||
|
||||
@@ -70,7 +70,10 @@ fn all_tools(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
ui.add_space(2.0);
|
||||
let shown = if app.all_tools_expanded { TOOL_GROUPS.len() } else { COLLAPSED_TOOLS.min(TOOL_GROUPS.len()) };
|
||||
for g in &TOOL_GROUPS[..shown] {
|
||||
if tool_row(ui, t, g).clicked() {
|
||||
let (resp, pin_clicked) = tool_row(ui, t, g, app.is_tool_pinned(g.id));
|
||||
if pin_clicked {
|
||||
app.toggle_pin_tool(g.id);
|
||||
} else if resp.clicked() {
|
||||
app.select_tool(g.id);
|
||||
}
|
||||
}
|
||||
@@ -102,21 +105,29 @@ pub(super) fn row_label(ui: &mut egui::Ui, text: &str, font: egui::FontId, color
|
||||
}
|
||||
|
||||
/// Draw one tool-group row with its icon, label, badge and availability hint.
|
||||
fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup) -> egui::Response {
|
||||
fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup, is_pinned: bool) -> (egui::Response, bool) {
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tl!(g.label)));
|
||||
let press = theme::Press::track(ui, &resp);
|
||||
press.wash(ui, rect, 6, false);
|
||||
let body = rect.translate(press.offset());
|
||||
icons::paint(ui, Rect::from_min_size(body.min + vec2(6.0, 7.0), vec2(20.0, 20.0)), g.icon, 19.0, hue(g));
|
||||
let right_reserved = match (g.badge, g.availability) {
|
||||
(Some(b), _) => ui.fonts_mut(|f| f.layout_no_wrap(tl!(b).to_owned(), theme::semibold(9.5), Color32::WHITE).size().x) + 26.0,
|
||||
|
||||
let pin_rect = Rect::from_center_size(body.right_center() - vec2(14.0, 0.0), vec2(22.0, 22.0));
|
||||
let pin_resp = ui.interact(pin_rect, ui.scope_id().with(("pin_toggle_row", g.id)), Sense::click());
|
||||
let pin_hovered = pin_resp.hovered();
|
||||
let mut pin_clicked = false;
|
||||
|
||||
let pin_space = 26.0;
|
||||
let badge_or_hint_reserved = match (g.badge, g.availability) {
|
||||
(Some(b), _) => ui.fonts_mut(|f| f.layout_no_wrap(tl!(b).to_owned(), theme::semibold(9.5), Color32::WHITE).size().x) + 18.0,
|
||||
(None, Availability::Planned(m)) if resp.hovered() => {
|
||||
let hint = format!("{} · {m}", tl!("Planned"));
|
||||
ui.fonts_mut(|f| f.layout_no_wrap(hint, theme::medium(10.5), t.text_faint).size().x) + 18.0
|
||||
ui.fonts_mut(|f| f.layout_no_wrap(hint, theme::medium(10.5), t.text_faint).size().x) + 10.0
|
||||
}
|
||||
_ => 8.0,
|
||||
_ => 0.0,
|
||||
};
|
||||
let right_reserved = pin_space + badge_or_hint_reserved + 8.0;
|
||||
let clipped =
|
||||
row_label(ui, tl!(g.label), theme::regular(13.5), t.text, body.left_center() + vec2(36.0, 0.0), rect.width() - 36.0 - right_reserved);
|
||||
match (g.badge, g.availability) {
|
||||
@@ -124,14 +135,14 @@ fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup) -> egui::Response {
|
||||
let font = theme::semibold(9.5);
|
||||
let badge = tl!(b);
|
||||
let w = ui.fonts_mut(|f| f.layout_no_wrap(badge.to_string(), font.clone(), Color32::WHITE).size().x);
|
||||
let r = Rect::from_center_size(body.right_center() - vec2(w / 2.0 + 10.0, 0.0), vec2(w + 10.0, 16.0));
|
||||
let r = Rect::from_center_size(body.right_center() - vec2(pin_space + w / 2.0 + 4.0, 0.0), vec2(w + 10.0, 16.0));
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), t.badge_new);
|
||||
ui.painter().text(r.center(), Align2::CENTER_CENTER, badge, font, Color32::WHITE);
|
||||
}
|
||||
// Planned tools: a quiet milestone hint instead of a chip, so the list stays calm.
|
||||
(None, Availability::Planned(m)) if resp.hovered() => {
|
||||
ui.painter().text(
|
||||
body.right_center() - vec2(10.0, 0.0),
|
||||
body.right_center() - vec2(pin_space + 4.0, 0.0),
|
||||
Align2::RIGHT_CENTER,
|
||||
format!("{} · {m}", tl!("Planned")),
|
||||
theme::medium(10.5),
|
||||
@@ -140,13 +151,34 @@ fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup) -> egui::Response {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let row_hovered = ui.rect_contains_pointer(rect) || resp.hovered();
|
||||
if is_pinned || row_hovered || pin_hovered {
|
||||
if pin_hovered {
|
||||
ui.painter().rect_filled(pin_rect, CornerRadius::same(4), t.pressed);
|
||||
}
|
||||
let pin_tint = if is_pinned {
|
||||
t.accent_text
|
||||
} else if pin_hovered {
|
||||
t.text
|
||||
} else {
|
||||
t.text_faint
|
||||
};
|
||||
icons::paint(ui, pin_rect.shrink(3.0), "pin", 15.0, pin_tint);
|
||||
let tip = if is_pinned { tl!("Unpin from Home") } else { tl!("Pin to Home") };
|
||||
pin_resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tip));
|
||||
if theme::hand(pin_resp).on_hover_text(tip).clicked() {
|
||||
pin_clicked = true;
|
||||
}
|
||||
}
|
||||
|
||||
let tip = match g.availability {
|
||||
Availability::Ready => tl!("Available").to_string(),
|
||||
Availability::Planned(m) => format!("{} {m} — {}", tl!("Planned for milestone"), tl!("open to see what it will include")),
|
||||
Availability::Provider => tl!("Optional: needs an AI provider you configure").into(),
|
||||
};
|
||||
let tip = if clipped { format!("{} — {tip}", tl!(g.label)) } else { tip };
|
||||
theme::hand(resp).on_hover_text(tip)
|
||||
(theme::hand(resp).on_hover_text(tip), pin_clicked)
|
||||
}
|
||||
|
||||
/// Draw the detail view for one tool group, dispatching its commands and tool-specific panels.
|
||||
|
||||
@@ -2,8 +2,8 @@ pub mod bulk_fields;
|
||||
pub(super) mod calc_order_ui;
|
||||
pub(super) mod camera_ui;
|
||||
pub mod comments;
|
||||
pub(super) mod content_ui;
|
||||
pub(super) mod edit_text_ui;
|
||||
pub mod content_ui;
|
||||
pub mod edit_text_ui;
|
||||
pub mod field_table_ui;
|
||||
pub mod fill_sign;
|
||||
pub mod forms_ui;
|
||||
|
||||
+1491
-1491
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -8023,7 +8023,7 @@ fn two_image_fixture() -> Vec<u8> {
|
||||
|
||||
#[test]
|
||||
fn duplicate_xobject_at_different_rects_arrange_and_delete_keeps_other_instance_through_menu() {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1400.0)).build_eframe(|_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("dup.pdf", None, two_image_fixture()).expect("opens fixture");
|
||||
@@ -8034,46 +8034,55 @@ fn duplicate_xobject_at_different_rects_arrange_and_delete_keeps_other_instance_
|
||||
assert!(h.state_mut().execute("edit.edit_text"));
|
||||
h.run_steps(2);
|
||||
|
||||
let r = h.state().views[0].page_screen_rect(0).expect("on screen");
|
||||
let info = h.state().session.get(id).unwrap().info.pages[0].clone();
|
||||
let user_to_screen = move |u: [f64; 2]| {
|
||||
let v = info.user_to_view(u[0] as f32, u[1] as f32);
|
||||
let scale = r.width() / info.width;
|
||||
egui::pos2(r.left() + v[0] * scale, r.top() + v[1] * scale)
|
||||
};
|
||||
let xf = h.state().views[0].page_xform(0).expect("page 0 xform");
|
||||
let imgs = h.state().session.get(id).unwrap().page_images(0);
|
||||
let b1 = pdfcraft_ui_egui::canvas::image_screen_rect(&imgs[1], &xf, &h.state().session.get(id).unwrap().info, 0);
|
||||
let p1 = b1.center();
|
||||
eprintln!("B1: {b1:?}, XF_RECT: {:?}, P1: {p1:?}", xf.rect);
|
||||
|
||||
let p1 = user_to_screen([110.0, 110.0]);
|
||||
// 1. Select B (image 1)
|
||||
h.hover_at(p1);
|
||||
h.run_steps(1);
|
||||
h.drag_at(p1);
|
||||
h.run_steps(1);
|
||||
h.drop_at(p1);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().views[0].image_selection.as_ref().map(|s| s.index), Some(1), "image 1 selected");
|
||||
|
||||
// 1. Right-click B (image 1) to open context menu
|
||||
// 2. Right-click B (image 1) to open context menu
|
||||
h.hover_at(p1);
|
||||
h.run_steps(1);
|
||||
h.event(egui::Event::PointerButton { pos: p1, button: egui::PointerButton::Secondary, pressed: true, modifiers: Default::default() });
|
||||
h.event(egui::Event::PointerButton { pos: p1, button: egui::PointerButton::Secondary, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(2);
|
||||
|
||||
// 2. Click "Bring to front" in context menu
|
||||
// 3. Open "Arrange image" submenu and click "Bring to front"
|
||||
h.get_by_label("Arrange image").click();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Bring to front").click();
|
||||
h.run_steps(3);
|
||||
|
||||
// 3. Selection must be preserved after arrange
|
||||
// 4. Selection must be preserved after arrange
|
||||
let sel = h.state().views[0].image_selection.as_ref().map(|s| s.index);
|
||||
assert!(sel.is_some(), "selection must be preserved after arrange");
|
||||
|
||||
// 4. Right-click on the selected image again to open context menu
|
||||
// 5. Right-click on the selected image again to open context menu
|
||||
let sel_idx = sel.unwrap();
|
||||
let b_rect = h.state().session.get(id).unwrap().page_images(0)[sel_idx].rect;
|
||||
let p_b = user_to_screen([(b_rect[0] + b_rect[2]) / 2.0, (b_rect[1] + b_rect[3]) / 2.0]);
|
||||
let xf_again = h.state().views[0].page_xform(0).expect("page 0 xform");
|
||||
let cur_imgs = h.state().session.get(id).unwrap().page_images(0);
|
||||
let b_rect = pdfcraft_ui_egui::canvas::image_screen_rect(&cur_imgs[sel_idx], &xf_again, &h.state().session.get(id).unwrap().info, 0);
|
||||
let p_b = b_rect.center();
|
||||
h.hover_at(p_b);
|
||||
h.run_steps(1);
|
||||
h.event(egui::Event::PointerButton { pos: p_b, button: egui::PointerButton::Secondary, pressed: true, modifiers: Default::default() });
|
||||
h.event(egui::Event::PointerButton { pos: p_b, button: egui::PointerButton::Secondary, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(2);
|
||||
|
||||
// 5. Click "Delete image" in context menu
|
||||
h.get_by_label("Delete image").click();
|
||||
// 6. Click "Delete" in context menu
|
||||
h.get_by_label("Delete").click();
|
||||
h.run_steps(3);
|
||||
|
||||
// 6. Save, reopen and verify: A remains at [20, 20, 40, 40], B is deleted
|
||||
// 7. Save, reopen and verify: A remains at [20, 20, 40, 40], B is deleted
|
||||
let saved = h.state().session.save_bytes(id).unwrap();
|
||||
let mut reopened = pdfcraft_engine::Session::new();
|
||||
let rid = reopened.open("again.pdf", None, saved, None).unwrap();
|
||||
|
||||
Reference in new issue
Block a user