Properties: Apply To Frame or Text for several text frames, as InDesign offers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
moppeeerandClaude Opus 5.5 committed 2026-10-20 22:34:16 +02:00
1 parent 8dff7ecdb0
commit 076de16622
2 files changed
+35 -2

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 › Appearance: with several text frames selected, the Fill and Stroke popups offer Apply To Frame or Text, as with one frame, so their text can be coloured at once, as in InDesign. 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.
+34 -2
View File
@@ -777,6 +777,10 @@ pub(crate) fn corner_type_dropdown(app: &mut DesignApp, ui: &mut Ui, shape: Corn
fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
let t = Tokens::get(ui.ctx());
let Some(doc) = app.session.active().map(|d| d.doc.clone()) else { return };
// Every selected object is a text frame: the popups offer Apply To Frame or Text, for one
// frame or several, as InDesign's do.
let text_frames =
i.is_text && app.session.active().is_some_and(|st| st.selection.items.iter().all(|id| doc.item(*id).is_some_and(|it| it.is_text_frame())));
// Fill.
let b = block(ui, FIELD_H);
let (fc, fg) = widgets::swatch_colors(&doc, &i.fill, 1.0);
@@ -789,7 +793,7 @@ fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
i.fill
));
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Fill swatch"));
swatch_menu(app, &resp, &doc, &i.fill, false, i.is_text && i.count == 1, false);
swatch_menu(app, &resp, &doc, &i.fill, false, text_frames, false);
crate::rtl::paint(
ui.painter(),
b.min + vec2(34.0, FIELD_H / 2.0),
@@ -811,7 +815,7 @@ fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
i.stroke
));
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Stroke swatch"));
swatch_menu(app, &resp, &doc, &i.stroke, true, i.is_text && i.count == 1, false);
swatch_menu(app, &resp, &doc, &i.stroke, true, text_frames, false);
if place(ui, sub(b, 34.0, 0.0, 56.0, FIELD_H), |ui| link_label(ui, crate::i18n::tr(&app.ui.language, "Stroke"), 56.0)).clicked() {
show_panel(app, "stroke");
}
@@ -3419,6 +3423,34 @@ mod tests {
assert_eq!(doc.item(designcraft_doc::ids::ItemId(id)).unwrap().fill.swatch, "[None]", "the frame isn't");
}
#[test]
fn apply_to_text_colours_the_text_of_several_text_frames() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
let mut ids = vec![];
let mut stories = vec![];
for x in [72, 250, 430] {
let r = app.run("frame.create", json!({"rect": [x, 72, x + 150, 140], "content": "text", "text": "75%"})).unwrap();
ids.push(r["id"].as_u64().unwrap());
stories.push(designcraft_doc::StoryId(r["story"].as_u64().unwrap()));
}
app.run("selection.set", json!({"ids": ids})).unwrap();
let mut h = panel(app);
h.get_by_label("Fill swatch").click();
h.run_steps(3);
// Three text frames, as one: Apply To is offered.
assert!(h.query_by_label("Apply To:").is_some());
h.get_by_label("Text").click();
h.run_steps(3);
h.get_by_label(CYAN).click();
h.run_steps(3);
let doc = &h.state().session.active().unwrap().doc;
for (id, story) in ids.iter().zip(&stories) {
assert_eq!(doc.story(*story).unwrap().char_format_at(0).over.fill.as_deref(), Some(CYAN), "every frame's text is cyan");
assert_eq!(doc.item(designcraft_doc::ids::ItemId(*id)).unwrap().fill.swatch, "[None]", "the frames aren't");
}
}
#[test]
fn transform_flips_and_rotates_from_properties() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());