mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
commit
4f193834bd
11 files changed
+419
-50
No files matched your search
@@ -141,6 +141,8 @@ Newest first.
|
||||
- **2026-10-14** · With the Page tool, the Control panel shows the page as InDesign's does: W/H from a reference point, page-size preset (US Business Card added), orientation, Liquid Page Rule and Show Parent Page Overlay, which draws the parent page's size over a resized page. No readiness percentage change.
|
||||
- **2026-10-14** · The Page tool works as InDesign's: a click selects a page and shows its handles and liquid page rule; dragging a handle previews the rule and springs back, Alt-drag resizes the page from the opposite edge, Shift keeps proportions, with a W/H readout. No readiness percentage change.
|
||||
- **2026-10-14** · With the Page tool, a guide dragged from a ruler is a liquid guide (dashed), as in InDesign; guide-based pages stretch the objects it crosses. No readiness percentage change.
|
||||
- **2026-10-14** · The Pages panel menu follows InDesign's order: Insert Pages…, Move Pages…, Duplicate Spread (and at End of Document), Delete Page/Spread, New Parent…, Apply Parent to Pages, Override All Parent Page Items, Parent Pages, Create Alternate Layout…, Numbering & Section Options…, Allow Selected Spread to Shuffle. No readiness percentage change.
|
||||
- **2026-10-14** · Pages panel: Alt-drag copies a page, a page dropped on Create new page is duplicated at the end, Edit page size offers the presets and Custom Page Size…; duplicated pages keep their own size, rule and guides; thumbnails of pages of their own size stay in their rows. No readiness percentage change.
|
||||
- **2026-10-12** · Text threads by hand: click a selected text frame's in or out port to load
|
||||
the text cursor, then click a frame to thread it, click empty space for a column-wide frame or
|
||||
drag one (it snaps like frame drawing); Shift-click autoflows, Alt-click stays loaded on the
|
||||
|
||||
@@ -135,21 +135,63 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
ok()
|
||||
})
|
||||
}),
|
||||
cmd!("layout.pages.duplicateSpread", "Duplicate Spread", ["Layout", "Pages"], None, "{spread}", has_doc, |s, p| {
|
||||
let si = p.get("spread").and_then(Value::as_u64).unwrap_or(0) as usize;
|
||||
s.edit(|d, _| {
|
||||
let sp = d.spreads.get(si).cloned().ok_or_else(|| bad("layout.pages.duplicateSpread", "no such spread"))?;
|
||||
let after = d.first_page_of_spread(si) + sp.pages.len() - 1;
|
||||
let parent = sp.pages.first().and_then(|p| p.parent);
|
||||
d.insert_pages(Some(after), sp.pages.len(), parent)?;
|
||||
// Copy items onto the new spread(s) at the same positions.
|
||||
let src = d.clone();
|
||||
let ids: Vec<_> = sp.items.iter().map(|i| i.id).collect();
|
||||
let (nsi, _) = d.page_loc(after + 1).ok_or_else(|| bad("layout.pages.duplicateSpread", "insert failed"))?;
|
||||
super::object::duplicate_from(d, &src, &ids, designcraft_doc::SpreadRef::Doc(nsi), designcraft_geom::Vec2::ZERO)?;
|
||||
ok()
|
||||
})
|
||||
}),
|
||||
cmd!(
|
||||
"layout.pages.duplicateSpread",
|
||||
"Duplicate Spread",
|
||||
["Layout", "Pages"],
|
||||
None,
|
||||
"{spread, atEnd?: bool (Duplicate Spread at End of Document; the Pages panel's New Page button takes a dropped page there), to?: page index the copied pages move to, in the same undo step (the Pages panel's Alt-drag)} → {page: the copy's first page (0-based)} — the spread's pages (size, margins, columns, guides, liquid rule) and items",
|
||||
has_doc,
|
||||
|s, p| {
|
||||
let si = p.get("spread").and_then(Value::as_u64).unwrap_or(0) as usize;
|
||||
let at_end = p.get("atEnd").and_then(Value::as_bool).unwrap_or(false);
|
||||
let to = p.get("to").and_then(Value::as_u64).map(|t| t as usize);
|
||||
s.edit(|d, _| {
|
||||
let sp = d.spreads.get(si).cloned().ok_or_else(|| bad("layout.pages.duplicateSpread", "no such spread"))?;
|
||||
// The copies land at `to`..`to + n` of the document with them in it.
|
||||
if to.is_some_and(|t| t > d.page_count()) {
|
||||
return Err(bad("layout.pages.duplicateSpread", "no such page"));
|
||||
}
|
||||
let after = if at_end { d.page_count() - 1 } else { d.first_page_of_spread(si) + sp.pages.len() - 1 };
|
||||
let parent = sp.pages.first().and_then(|p| p.parent);
|
||||
d.insert_pages(Some(after), sp.pages.len(), parent)?;
|
||||
// The copies are the pages they copy: size, margins, columns, guides, liquid rule.
|
||||
for (k, src) in sp.pages.iter().enumerate() {
|
||||
let Some((nsi, npi)) = d.page_loc(after + 1 + k) else { continue };
|
||||
let nsp = std::sync::Arc::make_mut(&mut d.spreads[nsi]);
|
||||
let pg = &mut nsp.pages[npi];
|
||||
let dx = pg.x - src.x;
|
||||
(pg.width, pg.height, pg.margins, pg.columns, pg.liquid) =
|
||||
(src.width, src.height, src.margins, src.columns.clone(), src.liquid);
|
||||
pg.guides = src.guides.clone();
|
||||
for g in pg.guides.iter_mut().filter(|g| g.orientation == designcraft_doc::Orientation::Vertical) {
|
||||
g.position += dx;
|
||||
}
|
||||
nsp.relayout();
|
||||
}
|
||||
// Copy items onto the new spread(s) at the same positions.
|
||||
let src = d.clone();
|
||||
let ids: Vec<_> = sp.items.iter().map(|i| i.id).collect();
|
||||
let (nsi, _) = d.page_loc(after + 1).ok_or_else(|| bad("layout.pages.duplicateSpread", "insert failed"))?;
|
||||
super::object::duplicate_from(d, &src, &ids, designcraft_doc::SpreadRef::Doc(nsi), designcraft_geom::Vec2::ZERO)?;
|
||||
let first = after + 1;
|
||||
let Some(to) = to else { return Ok(json!({"page": first})) };
|
||||
// Moving a page shifts the ones between; order the moves so each copy keeps
|
||||
// its place among the others.
|
||||
let n = sp.pages.len();
|
||||
if first < to {
|
||||
for k in (0..n).rev() {
|
||||
d.move_page(first + k, to + k)?;
|
||||
}
|
||||
} else {
|
||||
for k in 0..n {
|
||||
d.move_page(first + k, to + k)?;
|
||||
}
|
||||
}
|
||||
Ok(json!({"page": to}))
|
||||
})
|
||||
}
|
||||
),
|
||||
cmd!(
|
||||
"layout.pages.applyParent",
|
||||
"Apply Parent to Pages…",
|
||||
|
||||
@@ -724,6 +724,32 @@ fn page_tool_pins_and_locks_objects_on_object_based_pages() {
|
||||
assert!(s.overlays(v).iter().any(|o| matches!(o, designcraft_tools::Overlay::Tip { text, .. } if text.starts_with("Object is pinned"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicating_a_page_keeps_its_own_size_rule_guides_and_objects() {
|
||||
// Chris Cicchino's Page Tool video (2:06): the letterhead page is duplicated, then resized.
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 252, "height": 144, "facingPages": false})).unwrap();
|
||||
s.execute("layout.pages.insert", &json!({"count": 1})).unwrap();
|
||||
s.execute("layout.pageSize", &json!({"pages": [2], "width": 612, "height": 792})).unwrap();
|
||||
s.execute("liquid.pageRule", &json!({"rule": "scale", "pages": [2]})).unwrap();
|
||||
s.execute("guide.add", &json!({"spread": 1, "page": 0, "orientation": "vertical", "position": 300})).unwrap();
|
||||
s.execute("frame.create", &json!({"spread": 1, "rect": [100, 100, 200, 200]})).unwrap();
|
||||
let r = s.execute("layout.pages.duplicateSpread", &json!({"spread": 1})).unwrap();
|
||||
assert_eq!(r["page"], 2);
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let (si, pi) = d.page_loc(2).unwrap();
|
||||
let pg = &d.spreads[si].pages[pi];
|
||||
assert_eq!((pg.width, pg.height, pg.liquid), (612.0, 792.0, designcraft_doc::LiquidRule::Scale));
|
||||
assert_eq!(pg.guides.len(), 1);
|
||||
assert_eq!(d.spreads[si].items.len(), 1);
|
||||
// At the end of the document (the New Page button): the business card, not the last page's size.
|
||||
let r = s.execute("layout.pages.duplicateSpread", &json!({"spread": 0, "atEnd": true})).unwrap();
|
||||
assert_eq!(r["page"], 3);
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let (si, pi) = d.page_loc(3).unwrap();
|
||||
assert_eq!((d.spreads[si].pages[pi].width, d.spreads[si].pages[pi].height), (252.0, 144.0));
|
||||
}
|
||||
|
||||
/// Load the text from a frame's out port with the Selection tool and click on the pasteboard:
|
||||
/// a new frame is threaded there and the overset text flows into it.
|
||||
#[test]
|
||||
@@ -823,3 +849,31 @@ fn type_tool_frames_snap_to_the_margins() {
|
||||
assert!((r.x0 - margin.x0).abs() < 1e-6 && (r.y0 - margin.y0).abs() < 1e-6, "press point {r:?} vs {margin:?}");
|
||||
assert!((r.x1 - margin.x1).abs() < 1e-6, "right edge {} vs {}", r.x1, margin.x1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicating_a_page_to_an_index_is_one_undo_step() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"facingPages": false})).unwrap();
|
||||
s.execute("layout.pages.insert", &json!({"count": 2})).unwrap();
|
||||
for (page, width) in [(1, 300), (2, 400), (3, 500)] {
|
||||
s.execute("layout.pageSize", &json!({"pages": [page], "width": width, "height": 600})).unwrap();
|
||||
}
|
||||
let widths = |s: &Session| {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
(0..d.page_count()).map(|i| d.page_loc(i).map(|(si, pi)| d.spreads[si].pages[pi].width)).collect::<Option<Vec<_>>>().unwrap()
|
||||
};
|
||||
// Backwards: the last page copied to the front.
|
||||
let r = s.execute("layout.pages.duplicateSpread", &json!({"spread": 2, "to": 0})).unwrap();
|
||||
assert_eq!(r["page"], 0);
|
||||
assert_eq!(widths(&s), [500.0, 300.0, 400.0, 500.0]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(widths(&s), [300.0, 400.0, 500.0]);
|
||||
// Forwards: the first page copied to the end.
|
||||
s.execute("layout.pages.duplicateSpread", &json!({"spread": 0, "to": 3})).unwrap();
|
||||
assert_eq!(widths(&s), [300.0, 400.0, 500.0, 300.0]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(widths(&s), [300.0, 400.0, 500.0]);
|
||||
// Past the end: refused, nothing changes.
|
||||
assert!(s.execute("layout.pages.duplicateSpread", &json!({"spread": 0, "to": 4})).is_err());
|
||||
assert_eq!(widths(&s), [300.0, 400.0, 500.0]);
|
||||
}
|
||||
@@ -296,6 +296,28 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Document Setup", ["Document Setup", "Document Setup", "Document Setup", "Document Setup", "文档设置"]),
|
||||
("Drop Shadow", ["Drop Shadow", "Drop Shadow", "Drop Shadow", "Drop Shadow", "投影"]),
|
||||
("Duplicate Spread", ["Duplicate Spread", "Duplicate Spread", "Duplicate Spread", "Duplicate Spread", "复制跨页"]),
|
||||
(
|
||||
"Duplicate Spread at End of Document",
|
||||
[
|
||||
"Druckbogen am Dokumentende duplizieren",
|
||||
"Dupliquer la planche à la fin du document",
|
||||
"Duplicar pliego al final del documento",
|
||||
"ドキュメントの最後にスプレッドを複製",
|
||||
"在文档末尾复制跨页",
|
||||
],
|
||||
),
|
||||
("Delete Spread", ["Druckbogen löschen", "Supprimer la planche", "Eliminar pliego", "スプレッドを削除", "删除跨页"]),
|
||||
("Parent Pages", ["Übergeordnete Seiten", "Pages parents", "Páginas principales", "親ページ", "父页面"]),
|
||||
(
|
||||
"Apply Parent to Pages",
|
||||
[
|
||||
"Übergeordnete Seite auf Seiten anwenden",
|
||||
"Appliquer le parent aux pages",
|
||||
"Aplicar principal a páginas",
|
||||
"親ページを適用",
|
||||
"将父页面应用于页面",
|
||||
],
|
||||
),
|
||||
("Email", ["Email", "Email", "Email", "Email", "电子邮件"]),
|
||||
("Embed", ["Embed", "Embed", "Embed", "Embed", "嵌入"]),
|
||||
("Export PDF", ["PDF exportieren", "Exporter en PDF", "Exportar PDF", "PDF を書き出し", "导出PDF"]),
|
||||
@@ -612,6 +634,22 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Edit Page", ["Edit Page", "Edit Page", "Edit Page", "Edit Page", "编辑页面"]),
|
||||
("Import File", ["Import File", "Import File", "Import File", "Import File", "导入文件"]),
|
||||
("Insert Page", ["Insert Page", "Insert Page", "Insert Page", "Insert Page", "插入页面"]),
|
||||
(
|
||||
"Delete selected pages",
|
||||
["Ausgewählte Seiten löschen", "Supprimer les pages sélectionnées", "Eliminar páginas seleccionadas", "選択したページを削除", "删除选定页面"],
|
||||
),
|
||||
("Create new page", ["Neue Seite erstellen", "Créer une page", "Crear página nueva", "新規ページを作成", "新建页面"]),
|
||||
("Edit page size", ["Seitenformat bearbeiten", "Modifier le format de page", "Editar tamaño de página", "ページサイズを編集", "编辑页面大小"]),
|
||||
(
|
||||
"Custom Page Size…",
|
||||
[
|
||||
"Benutzerdefiniertes Seitenformat …",
|
||||
"Format de page personnalisé…",
|
||||
"Tamaño de página personalizado…",
|
||||
"カスタムページサイズ…",
|
||||
"自定页面大小…",
|
||||
],
|
||||
),
|
||||
("Hide Bar", ["Hide Bar", "Hide Bar", "Hide Bar", "Hide Bar", "隐藏栏"]),
|
||||
("Properties Panel", ["Properties Panel", "Properties Panel", "Properties Panel", "Properties Panel", "属性面板"]),
|
||||
("New Workspace…", ["New Workspace…", "New Workspace…", "New Workspace…", "New Workspace…", "新建工作区…"]),
|
||||
|
||||
@@ -270,6 +270,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Quick Actions", "الإجراءات السريعة"),
|
||||
("Import File", "استيراد ملف"),
|
||||
("Insert Page", "إدراج صفحة"),
|
||||
("Delete selected pages", "حذف الصفحات المحددة"),
|
||||
("Create new page", "إنشاء صفحة جديدة"),
|
||||
("Edit page size", "تحرير حجم الصفحة"),
|
||||
("Custom Page Size…", "حجم صفحة مخصص…"),
|
||||
("Edit Page", "تحرير الصفحة"),
|
||||
("Adjust Layout", "ضبط التخطيط"),
|
||||
("Smart Guides", "أدلة ذكية"),
|
||||
@@ -899,6 +903,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Document Grid", "شبكة المستند"),
|
||||
("Document Recovery Data", "بيانات استرداد المستند"),
|
||||
("Duplicate Spread", "تكرار الصفحات المتقابلة"),
|
||||
("Duplicate Spread at End of Document", "تكرار الانتشار في نهاية المستند"),
|
||||
("Delete Spread", "حذف الانتشار"),
|
||||
("Parent Pages", "الصفحات الأصل"),
|
||||
("Apply Parent to Pages", "تطبيق الأصل على الصفحات"),
|
||||
("Email", "بريد إلكتروني"),
|
||||
("Embed", "تضمين"),
|
||||
("Fast", "سريع"),
|
||||
|
||||
@@ -177,6 +177,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Document Setup", "Impostazione documento"),
|
||||
("Drop Shadow", "Ombra esterna"),
|
||||
("Duplicate Spread", "Duplica doppia pagina"),
|
||||
("Duplicate Spread at End of Document", "Duplica pagine affiancate alla fine del documento"),
|
||||
("Delete Spread", "Elimina pagine affiancate"),
|
||||
("Parent Pages", "Pagine principali"),
|
||||
("Apply Parent to Pages", "Applica pagina principale alle pagine"),
|
||||
("Email", "E-mail"),
|
||||
("Embed", "Incorpora"),
|
||||
("Export XML…", "Esporta XML…"),
|
||||
@@ -363,6 +367,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Edit Page", "Modifica pagina"),
|
||||
("Import File", "Importa file"),
|
||||
("Insert Page", "Inserisci pagina"),
|
||||
("Delete selected pages", "Elimina pagine selezionate"),
|
||||
("Create new page", "Crea nuova pagina"),
|
||||
("Edit page size", "Modifica dimensioni pagina"),
|
||||
("Custom Page Size…", "Dimensioni pagina personalizzate…"),
|
||||
("Hide Bar", "Nascondi barra"),
|
||||
("Properties Panel", "Pannello Proprietà"),
|
||||
("New Workspace…", "Nuova area di lavoro…"),
|
||||
|
||||
@@ -179,6 +179,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Document Setup", "ドキュメント設定"),
|
||||
("Drop Shadow", "ドロップシャドウ"),
|
||||
("Duplicate Spread", "スプレッドを複製"),
|
||||
("Duplicate Spread at End of Document", "ドキュメントの最後にスプレッドを複製"),
|
||||
("Delete Spread", "スプレッドを削除"),
|
||||
("Parent Pages", "親ページ"),
|
||||
("Email", "メール"),
|
||||
("Embed", "埋め込み"),
|
||||
("Export XML…", "XML を書き出し…"),
|
||||
@@ -365,6 +368,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Edit Page", "ページを編集"),
|
||||
("Import File", "ファイルを読み込み"),
|
||||
("Insert Page", "ページを挿入"),
|
||||
("Delete selected pages", "選択したページを削除"),
|
||||
("Create new page", "新規ページを作成"),
|
||||
("Edit page size", "ページサイズを編集"),
|
||||
("Custom Page Size…", "カスタムページサイズ…"),
|
||||
("Hide Bar", "バーを隠す"),
|
||||
("Properties Panel", "プロパティパネル"),
|
||||
("New Workspace…", "新規ワークスペース…"),
|
||||
|
||||
@@ -271,6 +271,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Quick Actions", "Ações rápidas"),
|
||||
("Import File", "Importar arquivo"),
|
||||
("Insert Page", "Inserir página"),
|
||||
("Delete selected pages", "Excluir páginas selecionadas"),
|
||||
("Create new page", "Criar nova página"),
|
||||
("Edit page size", "Editar tamanho da página"),
|
||||
("Custom Page Size…", "Tamanho de página personalizado…"),
|
||||
("Edit Page", "Editar página"),
|
||||
("Adjust Layout", "Ajustar layout"),
|
||||
("Smart Guides", "Guias inteligentes"),
|
||||
@@ -893,6 +897,18 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Document Grid", "Grade do documento"),
|
||||
("Document Recovery Data", "Dados de recuperação do documento"),
|
||||
("Duplicate Spread", "Duplicar página dupla"),
|
||||
("Insert Pages…", "Inserir páginas…"),
|
||||
("Move Pages…", "Mover páginas…"),
|
||||
("New Parent…", "Nova página principal…"),
|
||||
("Override All Parent Page Items", "Substituir todos os itens da página principal"),
|
||||
("Remove All Local Overrides", "Remover todas as substituições locais"),
|
||||
("Detach All Objects from Parent", "Desanexar todos os objetos da página principal"),
|
||||
("Create Alternate Layout…", "Criar layout alternativo…"),
|
||||
("Allow Selected Spread to Shuffle", "Permitir que a página espelhada selecionada seja reorganizada"),
|
||||
("Duplicate Spread at End of Document", "Duplicar página espelhada no final do documento"),
|
||||
("Delete Spread", "Excluir página espelhada"),
|
||||
("Parent Pages", "Páginas principais"),
|
||||
("Apply Parent to Pages", "Aplicar página principal às páginas"),
|
||||
("Email", "E-mail"),
|
||||
("Embed", "Embutir"),
|
||||
("Fast", "Rápido"),
|
||||
|
||||
@@ -180,6 +180,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Document Setup", "Параметри документа"),
|
||||
("Drop Shadow", "Тінь"),
|
||||
("Duplicate Spread", "Дублювати розворот"),
|
||||
("Duplicate Spread at End of Document", "Дублювати розворот у кінці документа"),
|
||||
("Delete Spread", "Видалити розворот"),
|
||||
("Parent Pages", "Батьківські сторінки"),
|
||||
("Apply Parent to Pages", "Застосувати батьківську до сторінок"),
|
||||
("Email", "Електронна пошта"),
|
||||
("Embed", "Вбудувати"),
|
||||
("Export XML…", "Експортувати XML…"),
|
||||
@@ -366,6 +370,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Edit Page", "Редагувати сторінку"),
|
||||
("Import File", "Імпортувати файл"),
|
||||
("Insert Page", "Вставити сторінку"),
|
||||
("Delete selected pages", "Видалити вибрані сторінки"),
|
||||
("Create new page", "Створити нову сторінку"),
|
||||
("Edit page size", "Змінити розмір сторінки"),
|
||||
("Custom Page Size…", "Власний розмір сторінки…"),
|
||||
("Hide Bar", "Сховати панель"),
|
||||
("Properties Panel", "Панель властивостей"),
|
||||
("New Workspace…", "Нове робоче середовище…"),
|
||||
|
||||
@@ -617,7 +617,7 @@ fn legacy_dock_menu_button_opens_the_active_panel_menu() {
|
||||
}
|
||||
h.run_steps(2);
|
||||
assert!(egui::Popup::is_any_open(&h.ctx), "the three-line button must open a menu");
|
||||
h.get_by_label("Insert Page After").click();
|
||||
h.get_by_label("Duplicate Spread").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().session.active().unwrap().doc.page_count(), pages + 1);
|
||||
assert!(!egui::Popup::is_any_open(&h.ctx));
|
||||
|
||||
@@ -114,11 +114,16 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let cur_spread = doc.page_loc(cur).map(|l| l.0);
|
||||
let center = ui.available_width() / 2.0;
|
||||
let dragging = egui::DragAndDrop::has_any_payload(ui.ctx());
|
||||
let tallest = doc.spreads.iter().flat_map(|sp| sp.pages.iter().map(|p| p.height)).fold(doc.settings.page_height, f64::max).max(1.0);
|
||||
for (si, sp) in doc.spreads.iter().enumerate() {
|
||||
let first = doc.first_page_of_spread(si);
|
||||
let scale = THUMB_H / doc.settings.page_height.max(1.0) as f32;
|
||||
// One scale for the whole document (pages of their own size show in proportion): the
|
||||
// document's page size at the usual height, unless a taller page would pass 1.8 times it.
|
||||
// Each row is as tall as its spread's tallest page.
|
||||
let scale = (THUMB_H / doc.settings.page_height.max(1.0) as f32).min(1.8 * THUMB_H / tallest as f32);
|
||||
let row_h = sp.pages.iter().map(|p| p.height as f32 * scale).fold(0.0, f32::max).max(THUMB_H * 0.25);
|
||||
let spine = sp.spine_x() as f32;
|
||||
let (row, _) = ui.allocate_exact_size(vec2(ui.available_width(), 8.0 + THUMB_H + 22.0), Sense::hover());
|
||||
let (row, _) = ui.allocate_exact_size(vec2(ui.available_width(), 8.0 + row_h + 22.0), Sense::hover());
|
||||
let top = row.min.y + 8.0;
|
||||
// Marker above the current spread.
|
||||
if cur_spread == Some(si) && !editing_parents {
|
||||
@@ -197,17 +202,57 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
.color(t.text_dim),
|
||||
);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if icons::button(ui, "trash", 20.0, false, crate::i18n::tr(&app.ui.language, "Delete Page")).clicked() {
|
||||
if icons::button(ui, "trash", 20.0, false, crate::i18n::tr(&app.ui.language, "Delete selected pages")).clicked() {
|
||||
let _ = app.run("layout.pages.delete", json!({"pages": [cur]}));
|
||||
}
|
||||
if icons::button(ui, "plus", 20.0, false, crate::i18n::tr(&app.ui.language, "Insert Page")).clicked() {
|
||||
// Create new page; a page dropped on it is duplicated at the end of the document.
|
||||
let plus = icons::button(ui, "plus", 20.0, dragging, crate::i18n::tr(&app.ui.language, "Create new page"));
|
||||
if let Some(payload) = plus.dnd_release_payload::<PageDrag>() {
|
||||
if let PageDrag::Page(from) = *payload
|
||||
&& let Some((si, _)) = doc.page_loc(from)
|
||||
{
|
||||
let _ = app.run("layout.pages.duplicateSpread", json!({"spread": si, "atEnd": true}));
|
||||
}
|
||||
} else if plus.clicked() {
|
||||
let _ = app.run("layout.pages.insert", json!({"after": cur, "count": 1}));
|
||||
}
|
||||
// Edit page size: InDesign's presets for the current page, or Custom Page Size….
|
||||
let size = icons::button(ui, "panel-pages", 20.0, false, crate::i18n::tr(&app.ui.language, "Edit page size"));
|
||||
egui::Popup::menu(&size).show(|ui| {
|
||||
let Some((si, pi)) = doc.page_loc(cur) else { return };
|
||||
let pg = &doc.spreads[si].pages[pi];
|
||||
let landscape = pg.width > pg.height;
|
||||
let current = crate::panels::properties::preset_name(pg.width, pg.height);
|
||||
for (name, w, h) in crate::panels::properties::PAGE_PRESETS {
|
||||
let (w, h) = if landscape { (h, w) } else { (w, h) };
|
||||
if ui.selectable_label(current == *name, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, name))).clicked() {
|
||||
let _ = app.run("layout.pageSize", json!({"pages": [cur + 1], "width": w, "height": h}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Custom Page Size…"))).clicked() {
|
||||
app.ui.dialog =
|
||||
Some(crate::dialogs::Dialog::new("cmd:layout.pageSize", json!({"pages": [cur + 1], "width": pg.width, "height": pg.height})));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// Alt-drag copies a page that is a spread of its own to where it's dropped, in one undo
|
||||
// step; a page from a larger spread moves as usual.
|
||||
let alt = ui.input(|i| i.modifiers.alt);
|
||||
let copy = |from: usize| {
|
||||
let (si, _) = doc.page_loc(from)?;
|
||||
(alt && doc.spreads.get(si).is_some_and(|s| s.pages.len() == 1)).then_some(si)
|
||||
};
|
||||
match drop {
|
||||
Some((PageDrag::Page(from), to)) if from != to => {
|
||||
let _ = app.run("layout.pages.move", json!({"from": from, "to": to}));
|
||||
Some((PageDrag::Page(from), to)) => {
|
||||
if let Some(si) = copy(from) {
|
||||
let _ = app.run("layout.pages.duplicateSpread", json!({"spread": si, "to": to}));
|
||||
} else if from != to {
|
||||
let _ = app.run("layout.pages.move", json!({"from": from, "to": to}));
|
||||
}
|
||||
}
|
||||
Some((PageDrag::Parent(prefix), to)) => {
|
||||
let _ = app.run("layout.pages.applyParent", json!({"pages": [to], "parent": prefix}));
|
||||
@@ -226,47 +271,43 @@ pub fn panel_menu(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
|
||||
fn page_menu(app: &mut DesignApp, ui: &mut egui::Ui, doc: &designcraft_doc::Document, abs: usize, si: usize) {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Insert Page After"))).clicked() {
|
||||
let _ = app.run("layout.pages.insert", json!({"after": abs, "count": 1}));
|
||||
let lang = app.ui.language.clone();
|
||||
let tr = |s: &str| crate::i18n::tr(&lang, s).to_owned();
|
||||
let pages_in_spread = doc.spreads.get(si).map_or(1, |sp| sp.pages.len());
|
||||
let item = |ui: &mut egui::Ui, label: &str| ui.button(crate::rtl::widget(ui, tr(label))).clicked();
|
||||
// InDesign's order: pages, parents, layouts and numbering, shuffling.
|
||||
if item(ui, "Insert Pages…") {
|
||||
app.ui.dialog = Some(crate::dialogs::Dialog::new("cmd:layout.pages.insert", json!({"after": abs, "count": 1})));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Page"))).clicked() {
|
||||
let _ = app.run("layout.pages.delete", json!({"pages": [abs]}));
|
||||
if item(ui, "Move Pages…") {
|
||||
app.ui.dialog = Some(crate::dialogs::Dialog::new("cmd:layout.pages.move", json!({"from": abs, "to": abs})));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Duplicate Spread"))).clicked() {
|
||||
if item(ui, "Duplicate Spread") {
|
||||
let _ = app.run("layout.pages.duplicateSpread", json!({"spread": si}));
|
||||
ui.close();
|
||||
}
|
||||
if si > 0
|
||||
&& ui
|
||||
.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Add to Previous Spread")))
|
||||
.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Spreads of up to 10 pages"));
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("layout.pages.toSpread", json!({"page": abs + 1, "spread": si - 1}));
|
||||
if item(ui, "Duplicate Spread at End of Document") {
|
||||
let _ = app.run("layout.pages.duplicateSpread", json!({"spread": si, "atEnd": true}));
|
||||
ui.close();
|
||||
}
|
||||
let mut shuffle = doc.spreads.get(si).is_none_or(|sp| sp.allow_shuffle);
|
||||
if ui.checkbox(&mut shuffle, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Allow Spread to Shuffle"))).clicked() {
|
||||
let _ = app.run("layout.spreadShuffle", json!({"spread": si, "allow": shuffle}));
|
||||
if item(ui, "Delete Page") {
|
||||
let _ = app.run("layout.pages.delete", json!({"pages": [abs]}));
|
||||
ui.close();
|
||||
}
|
||||
if pages_in_spread > 1 && item(ui, "Delete Spread") {
|
||||
let first = doc.first_page_of_spread(si);
|
||||
let _ = app.run("layout.pages.delete", json!({"pages": (first..first.saturating_add(pages_in_spread)).collect::<Vec<_>>()}));
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
for (id, label) in [
|
||||
("layout.overrideParentItems", "Override All Parent Page Items"),
|
||||
("layout.removeOverrides", "Remove All Local Overrides"),
|
||||
("layout.detachAll", "Detach All Objects from Parent"),
|
||||
] {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).clicked() {
|
||||
let _ = app.run(id, json!({"page": abs}));
|
||||
ui.close();
|
||||
}
|
||||
if item(ui, "New Parent…") {
|
||||
crate::menus::activate(app, "layout.parents.new", &serde_json::Value::Null);
|
||||
ui.close();
|
||||
}
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Apply Parent")), |ui| {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "[None]"))).clicked() {
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, tr("Apply Parent to Pages")), |ui| {
|
||||
if ui.button(crate::rtl::widget(ui, tr("[None]"))).clicked() {
|
||||
let _ = app.run("layout.pages.applyParent", json!({"pages": [abs], "parent": null}));
|
||||
ui.close();
|
||||
}
|
||||
@@ -278,6 +319,45 @@ fn page_menu(app: &mut DesignApp, ui: &mut egui::Ui, doc: &designcraft_doc::Docu
|
||||
}
|
||||
}
|
||||
});
|
||||
if item(ui, "Override All Parent Page Items") {
|
||||
let _ = app.run("layout.overrideParentItems", json!({"page": abs}));
|
||||
ui.close();
|
||||
}
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, tr("Parent Pages")), |ui| {
|
||||
for (id, label) in [("layout.removeOverrides", "Remove All Local Overrides"), ("layout.detachAll", "Detach All Objects from Parent")] {
|
||||
if ui.button(crate::rtl::widget(ui, tr(label))).clicked() {
|
||||
let _ = app.run(id, json!({"page": abs}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
if item(ui, "Create Alternate Layout…") {
|
||||
crate::menus::activate(app, "layout.createAlternate", &serde_json::Value::Null);
|
||||
ui.close();
|
||||
}
|
||||
if item(ui, "Numbering & Section Options…") {
|
||||
crate::menus::activate(app, "layout.section", &serde_json::Value::Null);
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
let mut shuffle = doc.spreads.get(si).is_none_or(|sp| sp.allow_shuffle);
|
||||
if ui.checkbox(&mut shuffle, crate::rtl::widget(ui, tr("Allow Selected Spread to Shuffle"))).clicked() {
|
||||
let _ = app.run("layout.spreadShuffle", json!({"spread": si, "allow": shuffle}));
|
||||
ui.close();
|
||||
}
|
||||
// Not in InDesign's menu (it drags pages into spreads): kept for spreads of up to 10 pages.
|
||||
if si > 0
|
||||
&& ui
|
||||
.button(crate::rtl::widget(ui, tr("Add to Previous Spread")))
|
||||
.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, tr("Spreads of up to 10 pages"));
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("layout.pages.toSpread", json!({"page": abs + 1, "spread": si - 1}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// A small label following the pointer while dragging.
|
||||
@@ -289,3 +369,109 @@ fn drag_ghost(ui: &egui::Ui, label: &str, t: &Tokens) {
|
||||
painter.rect(r, 2.0, t.panel_darker, Stroke::new(1.0, t.field_border), StrokeKind::Inside);
|
||||
painter.galley(r.min + vec2(5.0, 3.0), g, t.text_strong);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui_kittest::{Harness, kittest::Queryable};
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn harness(app: DesignApp) -> Harness<'static, DesignApp> {
|
||||
let mut h = Harness::builder().with_size(vec2(280.0, 520.0)).build_ui_state(
|
||||
|ui, app: &mut DesignApp| {
|
||||
let key = egui::Id::new("test_fonts");
|
||||
if ui.data(|d| d.get_temp::<bool>(key)).is_none() {
|
||||
crate::theme::install_fonts(ui.ctx(), "");
|
||||
ui.data_mut(|d| d.insert_temp(key, true));
|
||||
return;
|
||||
}
|
||||
show(app, ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
h
|
||||
}
|
||||
|
||||
/// Release a dragged page over `at`.
|
||||
fn drop_page(h: &mut Harness<'_, DesignApp>, page: usize, at: egui::Pos2, alt: bool) {
|
||||
let modifiers = egui::Modifiers { alt, ..Default::default() };
|
||||
h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: true, modifiers });
|
||||
egui::DragAndDrop::set_payload(&h.ctx, PageDrag::Page(page));
|
||||
h.event(egui::Event::PointerMoved(at));
|
||||
h.run_steps(2);
|
||||
h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers });
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
/// The page menu for page `abs` of spread `si`.
|
||||
fn menu_harness(app: DesignApp, abs: usize, si: usize) -> Harness<'static, DesignApp> {
|
||||
let mut h = Harness::builder().with_size(vec2(320.0, 600.0)).build_ui_state(
|
||||
move |ui, app: &mut DesignApp| {
|
||||
let key = egui::Id::new("test_fonts");
|
||||
if ui.data(|d| d.get_temp::<bool>(key)).is_none() {
|
||||
crate::theme::install_fonts(ui.ctx(), "");
|
||||
ui.data_mut(|d| d.insert_temp(key, true));
|
||||
return;
|
||||
}
|
||||
let doc = app.session.doc().unwrap().doc.clone();
|
||||
page_menu(app, ui, &doc, abs, si);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_page_in_a_spread_offers_delete_page_and_delete_spread() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"pages": 5, "facingPages": true})).unwrap();
|
||||
let doc = app.session.doc().unwrap().doc.clone();
|
||||
let (si, _) = doc.page_loc(2).unwrap();
|
||||
assert_eq!(doc.spreads[si].pages.len(), 2);
|
||||
let mut h = menu_harness(app, 2, si);
|
||||
let _ = h.get_by_label("Delete Spread");
|
||||
h.get_by_label("Delete Page").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().session.doc().unwrap().doc.page_count(), 4, "only the clicked page");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_panel_menu_follows_indesigns_order() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"pages": 2, "facingPages": false})).unwrap();
|
||||
let mut h = menu_harness(app, 0, 0);
|
||||
assert!(h.query_by_label("Delete Spread").is_none(), "a single-page spread has no Delete Spread");
|
||||
// InDesign's items, top to bottom.
|
||||
let ys: Vec<f32> = ["Insert Pages…", "Move Pages…", "Duplicate Spread", "Duplicate Spread at End of Document", "Delete Page", "New Parent…"]
|
||||
.iter()
|
||||
.map(|l| h.get_by_label(l).rect().center().y)
|
||||
.collect();
|
||||
assert!(ys.windows(2).all(|w| w[0] < w[1]), "{ys:?}");
|
||||
h.get_by_label("Duplicate Spread at End of Document").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().session.doc().unwrap().doc.page_count(), 3);
|
||||
h.get_by_label("Insert Pages…").click();
|
||||
h.run_steps(3);
|
||||
assert!(h.state().ui.dialog.as_ref().is_some_and(|d| d.id == "cmd:layout.pages.insert"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_page_dropped_on_create_new_page_is_duplicated_at_the_end() {
|
||||
// Chris Cicchino's Page Tool video (2:06): "click and drag onto my plus sign I can duplicate that".
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"width": 252, "height": 144, "facingPages": false})).unwrap();
|
||||
app.run("layout.pages.insert", json!({"count": 1})).unwrap();
|
||||
app.run("layout.pageSize", json!({"pages": [2], "width": 612, "height": 792})).unwrap();
|
||||
let mut h = harness(app);
|
||||
let plus = h.get_by_label("Create new page").rect().center();
|
||||
drop_page(&mut h, 1, plus, false);
|
||||
let d = h.state().session.doc().unwrap().doc.clone();
|
||||
assert_eq!(d.page_count(), 3);
|
||||
let (si, pi) = d.page_loc(2).unwrap();
|
||||
assert_eq!(d.spreads[si].pages[pi].width, 612.0, "a copy of the letterhead");
|
||||
let _ = h.get_by_label("Edit page size");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user