Merge Conflicts #4889

This commit is contained in:
Combiler01 committed 2026-10-27 11:04:20 +05:30
1 parent 2650cbc906
commit 5cf09552ab
16 files changed
+6207 -6159

No files matched your search

+5
View File
@@ -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)?;
+2 -1
View File
@@ -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 {
+2 -2
View File
@@ -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)]);
}
}
}
-10
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+6
View File
@@ -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)
}
+1 -1
View File
@@ -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)
}
}
+1
View File
@@ -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() {
+3 -3
View File
@@ -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;
+3
View File
@@ -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()
}
+1
View File
@@ -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 });
+41 -9
View File
@@ -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 -2
View File
@@ -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;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+27 -18
View File
@@ -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();