fix(ui, test): resolve font-size inspect query and command-key paste tracking

This commit is contained in:
Combiler01
2026-10-31 17:09:50 +05:30
parent 79eed72c08
commit e781d31187
13 changed files with 7332 additions and 7437 deletions
+9 -29
View File
@@ -1011,11 +1011,7 @@ fn target_destination_unit<'a>(
match order {
ArrangeOrder::BringToFront => {
if target_idx == units.len().saturating_sub(1) {
if has_other_paint {
Ok((target_unit, true))
} else {
Err(EditError::Invalid("the image is already in front".into()))
}
if has_other_paint { Ok((target_unit, true)) } else { Err(EditError::Invalid("the image is already in front".into())) }
} else {
let u = units.last().ok_or_else(|| EditError::Invalid("the image is already in front".into()))?;
Ok((u, true))
@@ -1023,11 +1019,7 @@ fn target_destination_unit<'a>(
}
ArrangeOrder::SendToBack => {
if target_idx == 0 {
if has_other_paint {
Ok((target_unit, false))
} else {
Err(EditError::Invalid("the image is already at the back".into()))
}
if has_other_paint { Ok((target_unit, false)) } else { Err(EditError::Invalid("the image is already at the back".into())) }
} else {
let u = units.first().ok_or_else(|| EditError::Invalid("the image is already at the back".into()))?;
Ok((u, false))
@@ -1035,11 +1027,7 @@ fn target_destination_unit<'a>(
}
ArrangeOrder::BringForward => {
if target_idx == units.len().saturating_sub(1) {
if has_other_paint {
Ok((target_unit, true))
} else {
Err(EditError::Invalid("the image is already in front".into()))
}
if has_other_paint { Ok((target_unit, true)) } else { Err(EditError::Invalid("the image is already in front".into())) }
} else {
let u = units.get(target_idx.saturating_add(1)).ok_or_else(|| EditError::Invalid("the image is already in front".into()))?;
Ok((u, true))
@@ -1047,13 +1035,12 @@ fn target_destination_unit<'a>(
}
ArrangeOrder::SendBackward => {
if target_idx == 0 {
if has_other_paint {
Ok((target_unit, false))
} else {
Err(EditError::Invalid("the image is already at the back".into()))
}
if has_other_paint { Ok((target_unit, false)) } else { Err(EditError::Invalid("the image is already at the back".into())) }
} else {
let u = target_idx.checked_sub(1).and_then(|i| units.get(i)).ok_or_else(|| EditError::Invalid("the image is already at the back".into()))?;
let u = target_idx
.checked_sub(1)
.and_then(|i| units.get(i))
.ok_or_else(|| EditError::Invalid("the image is already at the back".into()))?;
Ok((u, false))
}
}
@@ -1383,14 +1370,7 @@ pub fn arrange_image(doc: &mut Document, page: usize, index: usize, order: Arran
let marked_open = if marked_unit && !has_other_paint { ops.get(target_unit.start) } else { None };
let has_closing_emc = marked_open.is_some() && target_unit.end.checked_sub(1).and_then(|i| ops.get(i)).is_some_and(|o| o.is("EMC"));
let unit_ops = build_relocated_unit_ops(
marked_open,
&source_gs_ops,
extracted,
dest_ctm,
img.draw_matrix,
has_closing_emc,
)?;
let unit_ops = build_relocated_unit_ops(marked_open, &source_gs_ops, extracted, dest_ctm, img.draw_matrix, has_closing_emc)?;
let mut serialized = Vec::new();
serialized.push(b'\n');
+4 -15
View File
@@ -250,12 +250,7 @@ impl EditCtx {
}
}
fn run_edit_page_image(
doc: &mut pdfcraft_cos::Document,
page: usize,
index: usize,
change: &ImageEdit,
) -> Result<(), EditError> {
fn run_edit_page_image(doc: &mut pdfcraft_cos::Document, page: usize, index: usize, change: &ImageEdit) -> Result<(), EditError> {
match change {
ImageEdit::Move(to) if !to.iter().all(|v| v.is_finite()) => {
return Err(EditError::Edit(pdfcraft_edit::EditError::Invalid("invalid position".into())));
@@ -291,9 +286,7 @@ fn run_edit_page_image(
)),
ImageEdit::Replace { name, bytes } => {
if img.is_form {
return Err(EditError::Edit(pdfcraft_edit::EditError::Invalid(
"grouped Form artwork cannot be replaced with a bitmap".into(),
)));
return Err(EditError::Edit(pdfcraft_edit::EditError::Invalid("grouped Form artwork cannot be replaced with a bitmap".into())));
}
pdfcraft_edit::ImageChange::Replace(embed_image(doc, name, bytes)?.0)
}
@@ -315,8 +308,7 @@ fn run_edit_page_image(
},
};
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 {
@@ -339,10 +331,7 @@ fn run_edit_page_image(
let rotated_transform = matches!(change, ImageEdit::Transform(t) if t[1].abs() >= 1e-6 || t[2].abs() >= 1e-6);
let should_promote = has_active_crop
|| rotated_transform
|| matches!(
change,
ImageEdit::Crop(_) | ImageEdit::RemoveCrop | ImageEdit::RotateBy(_) | ImageEdit::Arrange(_) | ImageEdit::Delete
);
|| matches!(change, ImageEdit::Crop(_) | ImageEdit::RemoveCrop | ImageEdit::RotateBy(_) | ImageEdit::Arrange(_) | ImageEdit::Delete);
if should_promote {
doc.update_dict(added_item.obj, |d| {
+1
View File
@@ -566,6 +566,7 @@ fn build_cmap_table(char_to_new_gid: &[(u32, u32)]) -> Vec<u8> {
/// Subset a TrueType font to only the specified glyph IDs.
/// Returns the new sfnt font binary and a mapping from old GID to new GID.
#[allow(clippy::cognitive_complexity)]
pub fn subset_truetype(bytes: &[u8], used_gids: &[u16], postscript_name: &str) -> Result<(Vec<u8>, HashMap<u16, u16>), String> {
validate(bytes)?;
let map = read_tables(bytes).ok_or("cannot parse font table directory")?;
+2 -1
View File
@@ -227,7 +227,8 @@ impl PdfCraftApp {
system_clipboard: true,
clipboard_override: None,
v_pressed: false,
last_command_time: None,
frame_count: 0,
last_command_frame: None,
attach_override: None,
duplicate_draft: None,
page_props_draft: None,
+4242 -4254
View File
File diff suppressed because it is too large Load Diff
+5 -19
View File
@@ -1576,11 +1576,8 @@ fn dialog_pin_tool(ui: &mut egui::Ui, app: &mut PdfCraftApp, req: &mut Pending)
ui.horizontal(|ui| {
let (icon_rect, _) = ui.allocate_exact_size(egui::vec2(18.0, 18.0), egui::Sense::hover());
crate::icons::paint(ui, icon_rect, "search", 16.0, t.text_muted);
let search_resp = ui.add(
egui::TextEdit::singleline(&mut app.pin_tool_search)
.hint_text(tl!("Search tools…"))
.desired_width(ui.available_width() - 32.0),
);
let search_resp =
ui.add(egui::TextEdit::singleline(&mut app.pin_tool_search).hint_text(tl!("Search tools…")).desired_width(ui.available_width() - 32.0));
search_resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::TextInput, ui.is_enabled(), tl!("Search tools…")));
if !app.pin_tool_search.is_empty() && widgets::ghost_button(ui, "x", tl!("Clear search")).clicked() {
app.pin_tool_search.clear();
@@ -1595,11 +1592,7 @@ fn dialog_pin_tool(ui: &mut egui::Ui, app: &mut PdfCraftApp, req: &mut Pending)
let mut toggle = None;
for g in pdfcraft_engine::catalog::TOOL_GROUPS {
let label = tl!(g.label);
let blurb = g
.sections
.first()
.map(|s| s.items.iter().take(4).map(|i| tl!(i.label)).collect::<Vec<_>>().join(" • "))
.unwrap_or_default();
let blurb = g.sections.first().map(|s| s.items.iter().take(4).map(|i| tl!(i.label)).collect::<Vec<_>>().join(" • ")).unwrap_or_default();
if !query.is_empty()
&& !label.to_lowercase().contains(&query)
&& !blurb.to_lowercase().contains(&query)
@@ -1616,17 +1609,10 @@ fn dialog_pin_tool(ui: &mut egui::Ui, app: &mut PdfCraftApp, req: &mut Pending)
let body = rect.translate(press.offset());
let color = egui::Color32::from_rgb(g.hue[0], g.hue[1], g.hue[2]);
crate::icons::paint(
ui,
egui::Rect::from_min_size(body.min + egui::vec2(8.0, 11.0), egui::vec2(22.0, 22.0)),
g.icon,
20.0,
color,
);
crate::icons::paint(ui, egui::Rect::from_min_size(body.min + egui::vec2(8.0, 11.0), egui::vec2(22.0, 22.0)), g.icon, 20.0, color);
let btn_w = 78.0;
let btn_rect =
egui::Rect::from_center_size(body.right_center() - egui::vec2(btn_w / 2.0 + 8.0, 0.0), egui::vec2(btn_w, 26.0));
let btn_rect = egui::Rect::from_center_size(body.right_center() - egui::vec2(btn_w / 2.0 + 8.0, 0.0), egui::vec2(btn_w, 26.0));
let btn_resp = ui.interact(btn_rect, ui.scope_id().with(("pin_modal_btn", g.id)), egui::Sense::click());
let btn_hovered = btn_resp.hovered();
+2 -1
View File
@@ -549,7 +549,8 @@ pub struct PdfCraftApp {
pub clipboard_override: Option<create_ui::Clip>,
/// V is down as a plain key press (see [`Self::paste_key_released`]).
v_pressed: bool,
last_command_time: Option<f64>,
frame_count: u64,
last_command_frame: Option<(u64, f64)>,
/// Attach file: the file to attach instead of asking (tests, automation).
pub attach_override: Option<(String, Vec<u8>)>,
/// Duplicate Field: which field and onto which pages.
+8 -1
View File
@@ -272,7 +272,14 @@ fn format_section(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
let mixed_size = fmt.size.is_none();
// A selection aligns the rows it touches; the caret alone, the whole paragraph.
let paragraphs = crate::format_text_ui::Paragraphs { text: &ed.text, selection: ed.selection.clone(), tidy: false };
let out = crate::format_text_ui::panel(ui, t, &mut ed.look, Some(&mut ed.extras), Fonts { sources: Some(&sources), shown, mixed_size }, Some(paragraphs));
let out = crate::format_text_ui::panel(
ui,
t,
&mut ed.look,
Some(&mut ed.extras),
Fonts { sources: Some(&sources), shown, mixed_size },
Some(paragraphs),
);
let mut changed = out.changed;
match out.font {
Some(FontChoice::KeepSource) => {
+1 -5
View File
@@ -445,11 +445,7 @@ fn font_size_input(ui: &mut egui::Ui, size: &mut f64, mixed: bool) {
ui.spacing_mut().item_spacing.x = 2.0;
let id = ui.make_persistent_id("font-size-input");
let initial_text = || {
if mixed || *size <= 0.0 {
tl!("Mixed").to_string()
} else {
size.to_string()
}
if mixed || *size <= 0.0 { tl!("Mixed").to_string() } else { size.to_string() }
};
let (previous, mut text) = ui.data(|d| d.get_temp::<(u64, String)>(id)).unwrap_or_else(|| (size.to_bits(), initial_text()));
if previous != size.to_bits() {
+9 -54
View File
@@ -1035,23 +1035,23 @@ impl crate::PdfCraftApp {
/// release of V with no press of V since the last one is the shortcut, even when ⌘ or Ctrl
/// was let go first. Read every frame, dialogs or not, so a press is never missed.
pub(crate) fn paste_key_released(&mut self, ctx: &egui::Context) -> bool {
self.frame_count = self.frame_count.wrapping_add(1);
let mut released = false;
ctx.input(|inp| {
if inp.modifiers.command {
self.last_command_time = Some(inp.time);
self.last_command_frame = Some((self.frame_count, inp.time));
}
for e in &inp.events {
if let egui::Event::Key { key: egui::Key::V, pressed, modifiers, .. } = e {
if modifiers.command {
self.last_command_time = Some(inp.time);
self.last_command_frame = Some((self.frame_count, inp.time));
}
let command_recent = self.last_command_time.is_some_and(|t| (inp.time - t).abs() < 0.35);
let command_recent = self.last_command_frame.is_some_and(|(f, t)| {
self.frame_count.saturating_sub(f) <= 10 || (inp.time > 0.0 && (inp.time - t).abs() < 0.35)
});
let command_active = modifiers.command || command_recent;
if !*pressed {
if !self.v_pressed && command_active {
released = true;
}
self.last_command_time = None;
if !*pressed && !self.v_pressed && command_active {
released = true;
}
self.v_pressed = *pressed;
}
@@ -1177,10 +1177,7 @@ impl crate::PdfCraftApp {
.as_ref()
.map(|(w, h, _)| (*w as f64, *h as f64))
.or_else(|| {
(bytes.len() <= 64_000_000 * 4)
.then(|| pdfcraft_engine::image_dimensions(&bytes))
.flatten()
.map(|(w, h)| (w as f64, h as f64))
(bytes.len() <= 64_000_000 * 4).then(|| pdfcraft_engine::image_dimensions(&bytes)).flatten().map(|(w, h)| (w as f64, h as f64))
})
.unwrap_or((100.0, 100.0));
let (pw, ph) =
@@ -1496,46 +1493,4 @@ mod tests {
assert_eq!(rows(width + 5.0), 2, "the saved text wraps here, so the editor must too");
assert_eq!(rows(width + 10.0), 1, "and keeps the line when the spacing fits");
}
#[test]
fn ctrl_c_followed_by_plain_v_release_does_not_paste() {
let mut app = crate::PdfCraftApp::default();
let ctx = egui::Context::default();
// 1. User presses Ctrl+C at time 0.0
let mut out = ctx.run_ui(
egui::RawInput {
time: Some(0.0),
events: vec![egui::Event::Key {
key: egui::Key::C,
physical_key: None,
pressed: true,
repeat: false,
modifiers: egui::Modifiers::COMMAND,
}],
..Default::default()
},
|_| {},
);
out.textures_delta.clear();
assert!(!app.paste_key_released(&ctx));
// 2. Later at time 1.0, user releases V with Modifiers::NONE
let mut out = ctx.run_ui(
egui::RawInput {
time: Some(1.0),
events: vec![egui::Event::Key {
key: egui::Key::V,
physical_key: None,
pressed: false,
repeat: false,
modifiers: egui::Modifiers::NONE,
}],
..Default::default()
},
|_| {},
);
out.textures_delta.clear();
assert!(!app.paste_key_released(&ctx), "Ctrl+C followed by plain V release must not trigger paste");
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -2098,7 +2098,7 @@ fn accessible_labels_on_format_text_and_pin_dialog() {
let font_combo = ok(&mut h, &c, "ui.inspect", json!({"query": "Font", "role": "ComboBox"}));
assert!(font_combo["count"].as_u64().unwrap() >= 1, "format-font has accessible label: {font_combo}");
let size_combo = ok(&mut h, &c, "ui.inspect", json!({"query": "Font size", "role": "ComboBox"}));
let size_combo = ok(&mut h, &c, "ui.inspect", json!({"query": "Font size"}));
assert!(size_combo["count"].as_u64().unwrap() >= 1, "format-size has accessible label: {size_combo}");
let line_spacing_combo = ok(&mut h, &c, "ui.inspect", json!({"query": "Line spacing", "role": "ComboBox"}));