Properties: Text Wrap shows the four wrap offsets, as in InDesign

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
moppeeerandClaude Opus 5.5 committed 2026-10-20 18:35:46 +02:00
1 parent 8dff7ecdb0
commit 9b48a538d5
2 files changed
+100

No files matched your search

+1
View File
@@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **2026-10-20** · Properties › Text Wrap shows the four wrap offsets under the wrap buttons, with the link that makes them the same, as InDesign's Properties panel does (greyed with no wrap). No readiness percentage change.
- **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change.
- **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change.
- **2026-10-17** · File › Package writes the document's and the IDML's links relative to the package (`Links/logo.png`, `file:Links/logo.png`), so a package holds no path from the machine that made it; opening a document resolves them to the files beside it. Links › Copy Link(s) To still links by full path. (#340). No readiness percentage change.
+99
View File
@@ -1015,11 +1015,64 @@ fn wrap_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
let _ = app.run("object.textWrap", json!({"mode": i.wrap, "invert": inv}));
}
});
wrap_offsets(app, ui, i);
if more_options(ui, &app.ui.language).clicked() {
show_panel(app, "textWrap");
}
}
/// The wrap offsets under the wrap buttons, as InDesign's Properties panel shows them: Top and
/// Bottom, the link that makes all four the same, then Left and Right. Greyed with no wrap.
fn wrap_offsets(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
let t = Tokens::get(ui.ctx());
let lang = app.ui.language.clone();
let tr = |s: &str| crate::i18n::tr(&lang, s).to_string();
let u = units(app);
let enabled = i.wrap != "none";
let offsets = i.wrap_offsets;
ui.add_space(4.0);
let b = block(ui, 2.0 * FIELD_H + 5.0);
let mut set = None;
ui.add_enabled_ui(enabled, |ui| {
// Linked while the four are the same, until the link is clicked.
let linked = temp_bool(ui, "prop_wrap_linked", offsets.iter().all(|v| (v - offsets[0]).abs() < 1e-9));
// Offsets are [top, left, bottom, right].
for (k, key, icon, tip, x, y) in [
(0, "wrapTop", "margin-top", "Top", 0.0, 0.0),
(2, "wrapBottom", "margin-bottom", "Bottom", 0.0, FIELD_H + 5.0),
(1, "wrapLeft", "margin-inside", "Left", 140.0, 0.0),
(3, "wrapRight", "margin-outside", "Right", 140.0, FIELD_H + 5.0),
] {
let ir = sub(b, x, y + 2.0, 17.0, 17.0);
icons::paint(ui.painter(), ir, icon, if enabled { t.icon } else { t.text_dim });
ui.interact(ir, ui.id().with(("prop_wrap_off", k)), Sense::hover()).on_hover_ui(|ui| {
crate::rtl::label(ui, tr(tip));
});
let nv = place(ui, sub(b, x + 20.0, y, 76.0, FIELD_H), |ui| {
NumField::measure(key, Some(offsets[k]), u).width(76.0).spinner().range(-8000.0, 8000.0).enabled(enabled).show(ui)
});
if let Some(v) = nv {
let mut o = offsets;
if linked {
o = [v; 4];
} else {
o[k] = v;
}
set = Some(o);
}
}
if place(ui, sub(b, 106.0, (FIELD_H + 5.0) / 2.0, 22.0, FIELD_H), |ui| {
widgets::icon_toggle_sized(ui, if linked { "link" } else { "link-broken" }, linked, &tr("Make all settings the same"), vec2(22.0, 21.0))
.clicked()
}) {
set_temp_bool(ui, "prop_wrap_linked", !linked);
}
});
if let Some(o) = set {
let _ = app.run("object.textWrap", json!({"mode": i.wrap, "offset": o}));
}
}
fn arrange_menu(app: &mut DesignApp, resp: &egui::Response) {
egui::Popup::menu(resp).show(|ui| {
for (label, cmd) in [
@@ -3343,6 +3396,52 @@ mod tests {
assert!((r.x0 - 200.0).abs() < 0.5 && (r.y1 - 200.0).abs() < 0.5 && (r.x1 - 300.0).abs() < 0.5, "{r:?}");
}
/// Properties › Text Wrap shows the four offsets under the wrap buttons, as InDesign's
/// Properties panel does: linked at first, so one value sets all four; unlinked, one side.
#[test]
fn properties_text_wrap_shows_and_sets_the_offsets() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
let id = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "shape": "rectangle"})).unwrap()["id"].as_u64().unwrap();
app.run("selection.set", json!({"ids": [id]})).unwrap();
app.run("object.textWrap", json!({"mode": "boundingBox", "offset": 0})).unwrap();
let offsets = |app: &DesignApp| app.session.active().unwrap().doc.item(designcraft_doc::ids::ItemId(id)).unwrap().wrap.offsets;
let mut h = panel(app);
// The fields between the Text Wrap heading and Quick Actions: Top, Left, Bottom, Right.
let fields = |h: &Harness<'_, DesignApp>| {
let (top, bottom) = (h.get_by_label("Text Wrap").rect().bottom(), h.get_by_label("Quick Actions").rect().top());
let mut v: Vec<egui::Rect> = h
.get_all_by_role(egui::accesskit::Role::TextInput)
.map(|n| n.rect())
.filter(|r| r.center().y > top && r.center().y < bottom)
.collect();
v.sort_by(|a, b| (a.top(), a.left()).partial_cmp(&(b.top(), b.left())).unwrap());
v
};
let f = fields(&h);
assert_eq!(f.len(), 4, "four offsets: {f:?}");
let type_in = |h: &mut Harness<'_, DesignApp>, at: egui::Rect, text: &str| {
h.hover_at(at.center());
h.drag_at(at.center());
h.drop_at(at.center());
h.run_steps(2);
h.event(egui::Event::Key { key: egui::Key::A, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::COMMAND });
h.event(egui::Event::Text(text.into()));
h.key_press(egui::Key::Enter);
h.run_steps(3);
};
// Linked: the top offset sets all four.
type_in(&mut h, f[0], "12pt");
assert_eq!(offsets(h.state()), [12.0; 4]);
// Unlinked: the bottom one alone.
let (top, bottom) = (h.get_by_label("Text Wrap").rect().bottom(), h.get_by_label("Quick Actions").rect().top());
h.get_all_by_label("Make all settings the same").find(|n| n.rect().center().y > top && n.rect().center().y < bottom).unwrap().click();
h.run_steps(2);
let f = fields(&h);
type_in(&mut h, f[2], "6pt");
assert_eq!(offsets(h.state()), [12.0, 12.0, 6.0, 12.0], "[top, left, bottom, right]");
}
const CYAN: &str = "C=100 M=0 Y=0 K=0";
#[test]