mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-04 01:29:57 +00:00
fix(ui): Document Properties values line up and long fields wrap
The Description tab's plain values (File, Application, PDF producer) now start where the text inside the Title/Author fields does, the fields use the dialog's full width instead of a fixed 420 points, and Subject and Keywords wrap onto more lines instead of clipping mid-word. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
3 files changed
+48
-7
No files matched your search
@@ -118,11 +118,14 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
let Some((_, id)) = app.active_ids() else { return };
|
||||
let Some(doc) = app.session.get(id) else { return };
|
||||
let i = &doc.info;
|
||||
// Description shows plain values among text fields; they start where the fields' text does.
|
||||
let editable_info = tab == PropsTab::Description && doc.allows_modification() && app.props_draft.is_some();
|
||||
let inset = if editable_info { widgets::FIELD_TEXT_INSET } else { 0.0 };
|
||||
let row = |ui: &mut egui::Ui, k: &str, v: String| {
|
||||
// Row labels are UI chrome; values are document data and stay as they are.
|
||||
ui.label(egui::RichText::new(tl!(k)).color(t.text_muted));
|
||||
ui.scope(|ui| {
|
||||
ui.set_max_width(420.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(inset);
|
||||
ui.add(
|
||||
egui::Label::new(if v.is_empty() { egui::RichText::new("—").color(t.text_faint) } else { egui::RichText::new(v) }).wrap(),
|
||||
);
|
||||
@@ -134,11 +137,14 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
PropsTab::Description => {
|
||||
row(ui, "File", crate::bidi::visual(&doc.name).into_owned());
|
||||
match app.props_draft.as_mut() {
|
||||
Some((_, draft)) if doc.allows_modification() => {
|
||||
Some((_, draft)) if editable_info => {
|
||||
for (k, v) in INFO_KEYS.iter().zip(draft.iter_mut()) {
|
||||
let l = ui.label(egui::RichText::new(tl!(k)).color(t.text_muted));
|
||||
ui.add(widgets::line(v).desired_width(420.0).background_color(t.field).id_salt(("info", *k)))
|
||||
.labelled_by(l.id);
|
||||
// Subject and Keywords are often a sentence or a list: they wrap and grow
|
||||
// instead of hiding most of the value past the field's edge.
|
||||
let field =
|
||||
if matches!(*k, "Subject" | "Keywords") { widgets::block(v).desired_rows(1) } else { widgets::line(v) };
|
||||
ui.add(field.desired_width(f32::INFINITY).background_color(t.field).id_salt(("info", *k))).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ use crate::{PdfCraftApp, icons};
|
||||
|
||||
/// Horizontal and vertical inset of a single-line field.
|
||||
const FIELD_MARGIN: egui::Margin = egui::Margin::symmetric(10, 6);
|
||||
/// Where text starts inside a [`line`] or [`block`] field, so plain values beside fields can line up with it.
|
||||
pub(crate) const FIELD_TEXT_INSET: f32 = FIELD_MARGIN.left as f32;
|
||||
/// Outer height of a single-line field, including its inset.
|
||||
const FIELD_MIN_HEIGHT: f32 = 30.0;
|
||||
/// Padding inside every modal.
|
||||
|
||||
@@ -6,11 +6,17 @@ use egui_kittest::kittest::Queryable;
|
||||
use pdfcraft_ui_egui::PdfCraftApp;
|
||||
|
||||
fn fixture() -> Vec<u8> {
|
||||
let objs = [
|
||||
fixture_with_info(None)
|
||||
}
|
||||
|
||||
/// A one-page file, with `info` as its document information dictionary when given.
|
||||
fn fixture_with_info(info: Option<&str>) -> Vec<u8> {
|
||||
let mut objs = vec![
|
||||
"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 300] >>",
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 300] >>",
|
||||
];
|
||||
objs.extend(info);
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
@@ -22,7 +28,8 @@ fn fixture() -> Vec<u8> {
|
||||
for o in offsets {
|
||||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
|
||||
let info = if info.is_some() { " /Info 4 0 R" } else { "" };
|
||||
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R{info} >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
@@ -96,3 +103,29 @@ fn document_properties_long_filename_does_not_overflow_card() {
|
||||
let card = card(&h, "dialog");
|
||||
assert!(card.width() <= 700.0, "dialog card should not blow out horizontally on long filename, width was: {}", card.width());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_properties_values_line_up_and_long_fields_wrap() {
|
||||
let info = "<< /Title (Showcase) /Subject (A specimen document exercising typography, world scripts, vector art, forms and annotations) /Creator (demo-pdf) >>";
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("doc.pdf", None, fixture_with_info(Some(info))).unwrap();
|
||||
app.set_option("dialog", "properties").unwrap();
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
let card = card(&h, "dialog");
|
||||
let title = h.get_by_role_and_label(Role::TextInput, "Title").rect();
|
||||
// The fields reach the card's padding instead of stopping short of it.
|
||||
assert!(card.right() - title.right() <= 24.0, "title field {title:?} in {card:?}");
|
||||
// Plain values start where the fields' text does.
|
||||
for value in ["doc.pdf", "demo-pdf"] {
|
||||
// The file name is also the document tab's label.
|
||||
let v = h.query_all_by_label(value).map(|n| n.rect()).find(|r| card.contains_rect(*r)).unwrap_or_else(|| panic!("{value} in the dialog"));
|
||||
assert!((v.left() - (title.left() + 10.0)).abs() <= 1.0, "{value} at {v:?}, title field {title:?}");
|
||||
}
|
||||
// A long subject wraps onto a second line rather than running past the field's edge.
|
||||
let subject = h.get_by_role_and_label(Role::MultilineTextInput, "Subject").rect();
|
||||
assert!(subject.height() > title.height() + 8.0, "subject field {subject:?}, title field {title:?}");
|
||||
}
|
||||
Reference in new issue
Block a user