mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-03 06:51:50 -06:00
fix(ui, test): resolve font-size inspect query and command-key paste tracking
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+1808
-1807
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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"}));
|
||||
|
||||
Reference in New Issue
Block a user