mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 06:06:49 -06:00
TOC-5: integrate PR #892 section markers on current main
This commit is contained in:
@@ -410,6 +410,7 @@ Newest first.
|
||||
- **2026-10-15** · Flex Layout (InDesign 2026): Object › Flex Layout (Add to, Convert to, Remove) and a Flex Layout panel laid out as InDesign's, also in Properties. A container lays out its objects by direction, wrap, justification, the alignment grid, Fixed/Auto width and height, padding and spacing, after every edit; Add detects aligned objects (direction, Auto size, Space Between at their spacing); resizing a container reflows its objects instead of scaling them; dragging an object in one reorders it. Montilla Design's tutorial replays step for step. No readiness percentage change.
|
||||
- **2026-10-15** · IDML: the CJK leading model InDesign writes on [No paragraph style] of every document (Aki Below) only applies when the document uses a Japanese composer, as in InDesign; a Western document's paragraphs after a size change no longer sit lower than in InDesign (Montilla Design's Flex Layout file: its captions came out overset). No readiness percentage change.
|
||||
- **2026-10-15** · IDML: linked images in a subfolder beside the package load when the package is opened on another machine (a link to `/images/logo.png` finds `images/logo.png` next to the .idml); they showed as missing. No readiness percentage change.
|
||||
- **2026-10-14** · The Pages panel labels spreads as ranges or separate names across a section start, marks each section’s first page with a triangle, and scrolls to the page in view (#892). No readiness percentage change.
|
||||
- **2026-10-14** · Overset text is marked on every frame that ends an overset story, selected or not and inside groups too (the red mark at the out port, with frame edges shown); a selected group shows the in/out ports of the text frames inside it. InDesign shows it only on the selected frame, which hides overset labels in grouped objects until Preflight. No readiness percentage change.
|
||||
- **2026-10-14** · Load Swatches adds the colors of an `.ase` file that repeat a name or have none: a repeated name gets a number (`Red 2`), and a color without a name is named by its value (`C=0 M=0 Y=0 K=55`). Before, a repeated name kept one of its colors, and only the first unnamed color was added, with an empty name. No readiness percentage change.
|
||||
- **2026-10-14** · Objects snap to the bleed guide's four sides while moving, resizing or drawing; snapping and the canvas share one bleed rectangle (`Spread::bleed_rect`). No readiness percentage change.
|
||||
|
||||
@@ -204,6 +204,25 @@ const THUMB_H: f32 = 37.0;
|
||||
/// Parent rows.
|
||||
const ROW_H: f32 = 29.0;
|
||||
|
||||
/// A spread's label under its thumbnails, as InDesign's: one name for a single page, "2–3" for
|
||||
/// pages numbered on in one section (an en dash), and the names apart ("2, 1", "8, A") when a section starts
|
||||
/// inside the spread.
|
||||
pub(crate) fn spread_label(app: &DesignApp, doc: &designcraft_doc::Document, first: usize, n: usize) -> String {
|
||||
let names: Vec<String> = (first..first + n).map(|abs| app.session.page_label(abs)).collect();
|
||||
let starts = |abs: usize| abs == 0 || doc.sections.iter().any(|s| s.start == abs);
|
||||
match (names.first(), names.last()) {
|
||||
(Some(a), Some(b)) if names.len() > 1 => {
|
||||
if !app.session.prefs.absolute_page_numbers && (first + 1..first + n).any(starts) {
|
||||
names.join(", ")
|
||||
} else {
|
||||
format!("{a}\u{2013}{b}")
|
||||
}
|
||||
}
|
||||
(Some(a), _) => a.clone(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A row of the Pages panel's parents list: [None] (`parent` = `None`) or a parent.
|
||||
struct ParentRow {
|
||||
label: String,
|
||||
@@ -263,6 +282,7 @@ fn parent_icon(app: &mut DesignApp, ui: &egui::Ui, right: egui::Pos2, row: &Pare
|
||||
pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let uid = st.uid;
|
||||
let doc = st.doc.clone();
|
||||
let editing_parents = st.editing_parents;
|
||||
let aspect = (doc.settings.page_width / doc.settings.page_height.max(1.0)) as f32;
|
||||
@@ -396,6 +416,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
// ---- Document spreads.
|
||||
let cur = crate::canvas::current_page(app).unwrap_or(0);
|
||||
let cur_spread = doc.page_loc(cur).map(|l| l.0);
|
||||
let followed_id = egui::Id::new("pages_panel_followed");
|
||||
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);
|
||||
let th = opts.page_size.page_h();
|
||||
@@ -417,22 +438,13 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let spine = sp.spine_x() as f32;
|
||||
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 {
|
||||
let c = egui::pos2(row.min.x + center, row.min.y + 3.5);
|
||||
ui.painter().add(egui::Shape::convex_polygon(
|
||||
vec![c + vec2(-5.0, -3.0), c + vec2(5.0, -3.0), c + vec2(0.0, 3.0)],
|
||||
t.icon,
|
||||
Stroke::NONE,
|
||||
));
|
||||
let key = (uid, cur);
|
||||
if ui.data(|d| d.get_temp::<(u64, usize)>(followed_id)) != Some(key) {
|
||||
ui.scroll_to_rect(row, None);
|
||||
ui.data_mut(|d| d.insert_temp(followed_id, key));
|
||||
}
|
||||
}
|
||||
// Pages narrower than their numbers ("10" under an Extra Small page) are labelled
|
||||
// once for the spread.
|
||||
let narrow = sp.pages.len() > 1
|
||||
&& (first..first + sp.pages.len()).zip(&sp.pages).any(|(abs, p)| {
|
||||
let w = ui.painter().layout_no_wrap(app.session.page_label(abs), crate::theme::semibold(11.0), Color32::WHITE).size().x;
|
||||
(p.width as f32 * scale) < w + 6.0
|
||||
});
|
||||
for (pi, p) in sp.pages.iter().enumerate() {
|
||||
let abs = first + pi;
|
||||
let x0 = row.min.x + center + (p.x as f32 - spine) * scale;
|
||||
@@ -453,6 +465,15 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
ui.painter().rect_stroke(pr, 0.0, Stroke::new(0.5, Color32::from_gray(0x4a)), StrokeKind::Outside);
|
||||
if abs == 0 || doc.sections.iter().any(|s| s.start == abs) {
|
||||
let c = egui::pos2(pr.center().x, row.min.y + 4.0);
|
||||
ui.painter().add(egui::Shape::convex_polygon(
|
||||
vec![c + vec2(-4.5, -3.0), c + vec2(4.5, -3.0), c + vec2(0.0, 3.0)],
|
||||
t.icon,
|
||||
Stroke::NONE,
|
||||
));
|
||||
|
||||
}
|
||||
// Parent prefix in the outer top corner (black on white).
|
||||
if let Some(pid) = p.parent
|
||||
&& let Some(pp) = doc.parents.iter().find(|x| x.id == pid)
|
||||
@@ -490,33 +511,19 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
crate::canvas::go_to_page(app, abs);
|
||||
}
|
||||
resp.context_menu(|ui| page_menu(app, ui, &doc, abs, si));
|
||||
// Page number under the page: white on a blue badge when selected. Pages too
|
||||
// narrow for their numbers share one label under the spread (below).
|
||||
if narrow {
|
||||
continue;
|
||||
}
|
||||
let name = app.session.page_label(abs);
|
||||
let g = ui.painter().layout_no_wrap(name, crate::theme::semibold(11.0), Color32::WHITE);
|
||||
let c = egui::pos2(pr.center().x, pr.max.y + 11.0);
|
||||
if selected {
|
||||
let br = egui::Rect::from_center_size(c, vec2(g.size().x.max(7.0) + 7.0, 12.5));
|
||||
ui.painter().rect_filled(br, 0.0, BADGE);
|
||||
}
|
||||
ui.painter().galley(c - g.size() / 2.0, g, Color32::WHITE);
|
||||
}
|
||||
if narrow {
|
||||
let names: Vec<String> = (first..first + sp.pages.len()).map(|abs| app.session.page_label(abs)).collect();
|
||||
let label = match (names.first(), names.last()) {
|
||||
(Some(a), Some(b)) if names.len() > 1 => format!("{a}\u{2013}{b}"),
|
||||
(Some(a), _) => a.clone(),
|
||||
_ => String::new(),
|
||||
};
|
||||
let g = ui.painter().layout_no_wrap(label, crate::theme::semibold(11.0), Color32::WHITE);
|
||||
let c = egui::pos2(row.min.x + center, top + row_h + 11.0);
|
||||
if cur_spread == Some(si) && !editing_parents {
|
||||
ui.painter().rect_filled(egui::Rect::from_center_size(c, vec2(g.size().x + 7.0, 12.5)), 0.0, BADGE);
|
||||
}
|
||||
ui.painter().galley(c - g.size() / 2.0, g, Color32::WHITE);
|
||||
let label = spread_label(app, &doc, first, sp.pages.len());
|
||||
let g = ui.painter().layout_no_wrap(label.clone(), crate::theme::semibold(11.0), Color32::WHITE);
|
||||
let c = egui::pos2(row.min.x + center, top + row_h + 11.0);
|
||||
let br = egui::Rect::from_center_size(c, vec2(g.size().x + 7.0, 12.5));
|
||||
if cur_spread == Some(si) && !editing_parents {
|
||||
ui.painter().rect_filled(br, 0.0, BADGE);
|
||||
}
|
||||
ui.painter().galley(c - g.size() / 2.0, g, Color32::WHITE);
|
||||
let lr = ui.interact(br, ui.id().with(("spread_label", si)), Sense::click());
|
||||
lr.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Label, true, &label));
|
||||
if lr.clicked() {
|
||||
crate::canvas::go_to_page(app, first);
|
||||
}
|
||||
}
|
||||
// Right-click on the empty part of the panel: the short menu.
|
||||
@@ -1108,3 +1115,64 @@ mod tests {
|
||||
let _ = h.get_by_label("Edit page size");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod spread_label_tests {
|
||||
use egui_kittest::{Harness, kittest::Queryable};
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn app() -> DesignApp {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"facingPages": true, "pages": 9})).unwrap();
|
||||
app
|
||||
}
|
||||
|
||||
fn labels(app: &DesignApp) -> Vec<String> {
|
||||
let doc = app.session.doc().unwrap().doc.clone();
|
||||
(0..doc.spreads.len()).map(|si| spread_label(app, &doc, doc.first_page_of_spread(si), doc.spreads[si].pages.len())).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spreads_are_labelled_as_indesign_labels_them() {
|
||||
// Chris Nielsen's "Numbering Page Sections": sections starting at 1 on pages 3 and 7,
|
||||
// the second in letters, label their spreads "2, 1" and "6, A".
|
||||
let mut app = app();
|
||||
assert_eq!(labels(&app), ["1", "2–3", "4–5", "6–7", "8–9"]);
|
||||
app.run("layout.section", json!({"page": 3, "startNumber": 1})).unwrap();
|
||||
app.run("layout.section", json!({"page": 7, "startNumber": 1, "style": "upperLetters"})).unwrap();
|
||||
assert_eq!(labels(&app), ["1", "2, 1", "2–3", "4, A", "B–C"]);
|
||||
// Absolute numbering: positions, always a range.
|
||||
app.session.prefs.absolute_page_numbers = true;
|
||||
assert_eq!(labels(&app), ["1", "2–3", "4–5", "6–7", "8–9"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_panel_shows_spread_labels_and_a_click_on_one_goes_to_its_spread() {
|
||||
let mut app = app();
|
||||
app.run("layout.section", json!({"page": 3, "startNumber": 1})).unwrap();
|
||||
// The canvas the panel's clicks move (not drawn here).
|
||||
app.canvas_rect = Some(egui::Rect::from_min_size(egui::pos2(0.0, 0.0), vec2(1000.0, 800.0)));
|
||||
let mut h = Harness::builder().with_size(vec2(260.0, 900.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;
|
||||
}
|
||||
egui::ScrollArea::vertical().show(ui, |ui| show(app, ui));
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
let _ = h.get_by_label("2, 1");
|
||||
assert!(h.query_by_label("2 1").is_none());
|
||||
// Each page is named for screen readers: two pages are numbered 1.
|
||||
assert_eq!(h.query_all_by_label("Page 1").count(), 2);
|
||||
h.get_by_label("2–3").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().session.doc().unwrap().doc.page_loc(crate::canvas::current_page(h.state()).unwrap()).unwrap().0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user