diff --git a/ROADMAP.md b/ROADMAP.md index 8a0f82071..050c07d9a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,6 +129,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **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-12** · macOS Dock Quit uses the same unsaved-document prompts as window and menu Quit. Repeated requests keep the current answers; Cancel retains every tab and unsaved recovery checkpoint, and completed Save/Don't Save answers exit through the ordinary event loop. No readiness percentage change. - **2026-10-12** · Orderly native exit writes the latest recovery state after finalizing current quit approvals; incomplete or stale plans keep their documents, and Undo to the saved state clears stale recovery. Unavailable or failed recovery is reported. Cancelable macOS Dock Quit remains open (#544). No readiness percentage change. - **2026-10-12** · Recovered documents keep a checkpoint until saved, closed or reverted, including a second crash before autosave. Restarted document IDs and explicit recovery folders preserve other snapshots; failed republishing retains the original. No readiness percentage change. diff --git a/crates/doc/src/selection.rs b/crates/doc/src/selection.rs index d9e9b7a3d..2d28bf2c2 100644 --- a/crates/doc/src/selection.rs +++ b/crates/doc/src/selection.rs @@ -80,6 +80,9 @@ pub struct Selection { /// Key object for Align. #[serde(default, skip_serializing_if = "Option::is_none")] pub key: Option, + /// The page selected with the Page tool (absolute index). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub page: Option, } impl Selection { diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index bd5320799..2c23640f1 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -376,10 +376,24 @@ pub fn specs() -> Vec { "Page Size", [], None, - "{pages: [1-based page numbers], width?, height?, preset?: \"A4\"|\"Letter\"|…} — pages of their own size (Page tool); objects on the pages keep their place on them", + "{pages: [1-based page numbers], width?, height?, preset?: \"A4\"|\"Letter\"|…, anchor?: 0–8 (reference point that stays put, row-major from the top-left; default 0)} — pages of their own size (Page tool); objects keep their place relative to the anchor, or follow the page's liquid rule", has_doc, page_size ), + cmd!(noundo "layout.selectPage", "Select Page", [], None, "{page: 1-based | null} — the page the Page tool selects (its handles, the Control panel)", has_doc, |s, p| { + let n = s.doc()?.doc.page_count(); + let page = match p.get("page") { + None | Some(Value::Null) => None, + Some(v) => Some(v.as_u64().filter(|v| *v >= 1 && *v as usize <= n).ok_or_else(|| bad("layout.selectPage", format!("`page`: 1–{n}")))? as usize - 1), + }; + let st = s.doc_mut()?; + if page.is_some() { + st.selection = Default::default(); + } + st.selection.page = page; + st.revision += 1; + Ok(json!({"page": page.map(|p| p + 1)})) + }), cmd!( "layout.section", "Numbering & Section Options…", @@ -640,8 +654,12 @@ fn page_size(s: &mut crate::Session, p: &Value) -> Result { w = Some(pr.width); h = Some(pr.height); } + let anchor = match p.get("anchor") { + None | Some(Value::Null) => 0, + Some(v) => v.as_u64().filter(|a| *a <= 8).ok_or_else(|| bad("layout.pageSize", "`anchor`: 0–8"))? as usize, + }; s.edit(|d, _| { - resize_pages(d, &pages, w, h); + resize_pages_from(d, &pages, w, h, anchor); Ok(json!({"pages": pages.len()})) }) } @@ -649,14 +667,35 @@ fn page_size(s: &mut crate::Session, p: &Value) -> Result { /// Give `pages` (1-based) their own size. Objects stay on their page; pages with a liquid rule /// re-flow their objects by it. pub(crate) fn resize_pages(d: &mut designcraft_doc::Document, pages: &[usize], w: Option, h: Option) { + resize_pages_from(d, pages, w, h, 0); +} + +/// [`resize_pages`] keeping reference point `anchor` (0–8, row-major from the top-left) in place: +/// the page grows away from it, so objects keep their place relative to it. +pub(crate) fn resize_pages_from(d: &mut designcraft_doc::Document, pages: &[usize], w: Option, h: Option, anchor: usize) { + let (fx, fy) = ((anchor % 3) as f64 / 2.0, (anchor / 3).min(2) as f64 / 2.0); { for page in pages { let Some((si, pi)) = d.page_loc(page - 1) else { continue }; let sp = std::sync::Arc::make_mut(&mut d.spreads[si]); let old: Vec = sp.pages.iter().map(|x| x.x).collect(); let old_bounds = sp.pages[pi].bounds(); + // The page's own objects (outer pages take the pasteboard beside them). + let last = sp.pages.len() - 1; + let (lo, hi) = (if pi == 0 { f64::NEG_INFINITY } else { old_bounds.x0 }, if pi == last { f64::INFINITY } else { old_bounds.x1 }); + let own: Vec = sp + .items + .iter() + .enumerate() + .filter(|(_, it)| { + let c = it.bounds().center().x; + c >= lo && c < hi + }) + .map(|(i, _)| i) + .collect(); let pg = &mut sp.pages[pi]; let dw = w.map_or(0.0, |w| w.max(1.0) - pg.width); + let dh = h.map_or(0.0, |h| h.max(1.0) - pg.height); if let Some(w) = w { pg.width = w.max(1.0); } @@ -693,8 +732,22 @@ pub(crate) fn resize_pages(d: &mut designcraft_doc::Document, pages: &[usize], w } } } - // Liquid rules: objects were carried along with the page's left edge. - let moved = old_bounds + designcraft_geom::Vec2::new(new[pi] - old[pi], 0.0); + // Growing away from the anchor: the page's objects and guides keep their place + // relative to it. + let shift = designcraft_geom::Vec2::new(fx * dw, fy * dh); + if shift != designcraft_geom::Vec2::ZERO { + for i in &own { + if let Some(it) = sp.items.get_mut(*i) { + let it = std::sync::Arc::make_mut(it); + it.xf = designcraft_geom::Affine::translate(shift) * it.xf; + } + } + for g in &mut sp.pages[pi].guides { + g.position += if g.orientation == designcraft_doc::Orientation::Vertical { shift.x } else { shift.y }; + } + } + // Liquid rules: objects were carried along with the page's left edge (and the anchor). + let moved = old_bounds + designcraft_geom::Vec2::new(new[pi] - old[pi], 0.0) + shift; super::liquid::apply(d, designcraft_doc::SpreadRef::Doc(si), pi, moved); } } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 17d1ed098..deec4b9b1 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -569,3 +569,76 @@ fn commands_take_non_object_params_without_panicking() { assert!(r.is_ok(), "{p}: {r:?}"); } } + +#[test] +fn page_tool_selects_a_page_and_its_handles_preview_or_resize_it() { + use designcraft_tools::{CanvasLayout, Mods, PointerEvent, PointerKind}; + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 612, "height": 792, "facingPages": false})).unwrap(); + let id = s.execute("frame.create", &json!({"rect": [400, 100, 500, 200]})).unwrap()["id"].as_u64().unwrap(); + let frame = |s: &Session| s.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().bounds(); + let width = |s: &Session| s.doc().unwrap().doc.spreads[0].pages[0].width; + // The page on the canvas (a single page is centred on the spine). + let page = |s: &Session| { + let d = &s.doc().unwrap().doc; + CanvasLayout::new(d, false).page_rect(d, 0).unwrap() + }; + s.set_tool("page"); + let v = ViewInfo::at_zoom(1.0); + let ev = |kind, x: f64, y: f64, alt: bool| PointerEvent { mods: Mods { alt, ..Default::default() }, ..PointerEvent::new(kind, x, y) }; + let drag = |s: &mut Session, from: (f64, f64), by: f64, alt: bool, check: &dyn Fn(&Session)| { + s.pointer(&ev(PointerKind::Down, from.0, from.1, alt), v).unwrap(); + s.pointer(&ev(PointerKind::Drag, from.0 + by, from.1, alt), v).unwrap(); + check(s); + s.pointer(&ev(PointerKind::Up, from.0 + by, from.1, alt), v).unwrap(); + }; + // A click selects the page. + let c = page(&s).center(); + s.pointer(&ev(PointerKind::Down, c.x, c.y, false), v).unwrap(); + s.pointer(&ev(PointerKind::Up, c.x, c.y, false), v).unwrap(); + assert_eq!(s.doc().unwrap().selection.page, Some(0)); + // A plain drag of the right handle previews the page's liquid rule and springs back. + s.execute("liquid.pageRule", &json!({"rule": "scale"})).unwrap(); + let undo = s.doc().unwrap().history.undo.len(); + let r = page(&s); + drag(&mut s, (r.x1, r.center().y), 612.0, false, &|s| { + assert_eq!(width(s), 1224.0, "previewed"); + // Scale keeps proportions: only wider, so the frame is centred on the wider page. + assert_eq!(frame(s).x0, 706.0, "the Scale rule in the preview: {:?}", frame(s)); + }); + assert_eq!((width(&s), frame(&s)), (612.0, designcraft_geom::Rect::new(400.0, 100.0, 500.0, 200.0)), "sprung back"); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "nothing to undo"); + // Alt-drag resizes; with the rule off the frame stays where it is. + s.execute("liquid.pageRule", &json!({"rule": "off"})).unwrap(); + let r = page(&s); + drag(&mut s, (r.x1, r.center().y), 100.0, true, &|_| {}); + assert_eq!(width(&s), 712.0); + assert_eq!(frame(&s).x0, 400.0); + // The left handle grows the page to the left: the frame keeps its place by the right edge. + let r = page(&s); + drag(&mut s, (r.x0, r.center().y), -100.0, true, &|_| {}); + let pg = s.doc().unwrap().doc.spreads[0].pages[0].clone(); + assert_eq!(pg.width, 812.0); + assert_eq!(frame(&s).x0 - pg.x, 500.0, "100 pt further from the page's left edge"); + // A click on the pasteboard deselects the page. + let r = page(&s); + s.pointer(&ev(PointerKind::Down, r.x0 - 200.0, r.center().y, false), v).unwrap(); + assert_eq!(s.doc().unwrap().selection.page, None); +} + +#[test] +fn page_size_keeps_objects_by_its_anchor() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 400, "facingPages": false})).unwrap(); + let id = s.execute("frame.create", &json!({"rect": [100, 100, 200, 200]})).unwrap()["id"].as_u64().unwrap(); + let frame = |s: &Session| s.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().bounds(); + // Bottom-right stays put: 100 wider and 50 taller moves the frame 100 right and 50 down. + s.execute("layout.pageSize", &json!({"pages": [1], "width": 500, "height": 450, "anchor": 8})).unwrap(); + assert_eq!(frame(&s), designcraft_geom::Rect::new(200.0, 150.0, 300.0, 250.0)); + // The centre: half of it. + s.execute("layout.pageSize", &json!({"pages": [1], "width": 700, "height": 450, "anchor": 4})).unwrap(); + assert_eq!(frame(&s).x0, 300.0); + assert!(s.execute("layout.pageSize", &json!({"pages": [1], "width": 700, "anchor": 9})).is_err()); + assert!(s.execute("layout.selectPage", &json!({"page": 2})).is_err()); + assert_eq!(s.execute("layout.selectPage", &json!({"page": 1})).unwrap()["page"], 1); +} diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index f3de390a4..dd09e9c25 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -314,6 +314,14 @@ pub enum Overlay { b: Point, label: String, }, + /// A page selected with the Page tool (canvas rect): its outline and eight handles. + PageHandles(Rect), + /// A small tag whose bottom-left is at `p` (the Page tool's liquid page rule; English, the UI + /// translates it). + Tag { + p: Point, + text: String, + }, } /// Correction from a snap, in spread coordinates for `delta`. @@ -400,7 +408,7 @@ pub fn create(id: &str) -> Box { "hand" => Box::new(nav::HandTool::default()), "placeGun" => Box::new(nav::PlaceGun::default()), "zoom" => Box::new(nav::ZoomTool::default()), - "page" => Box::new(nav::PageTool), + "page" => Box::new(nav::PageTool::default()), "typeOnPath" => Box::new(nav::PathTypeTool), "gap" => Box::new(nav::GapTool::default()), "contentCollector" => Box::new(nav::ConveyorTool::new(false)), diff --git a/crates/tools/src/nav.rs b/crates/tools/src/nav.rs index d892b090a..a94a93881 100644 --- a/crates/tools/src/nav.rs +++ b/crates/tools/src/nav.rs @@ -3,7 +3,7 @@ use designcraft_geom::Point; use serde_json::json; -use crate::{Action, Cursor, Mods, PointerEvent, PointerKind, Tool, ToolContext}; +use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext}; /// Hand tool: drag to pan. Alt-press starts Power Zoom: the view zooms out to the spread, the /// pointer aims a rectangle of the previous view, and releasing zooms back in there. @@ -162,27 +162,161 @@ impl Tool for PlaceGun { } } -/// Page tool (Shift+P): click a page to set its own size. +/// Page tool (Shift+P): click a page to select it; drag one of its handles to preview its liquid +/// page rule (the page springs back on release), Alt-drag to resize it, Shift to keep its +/// proportions; double-click a page for exact sizes. #[derive(Default)] -pub struct PageTool; +pub struct PageTool { + drag: Option, +} + +/// A page handle being dragged. +#[derive(Clone, Copy, Debug)] +struct PageDrag { + /// Absolute page index. + page: usize, + /// Reference-point index of the handle (0–8, row-major; 4 is the centre and has none). + handle: usize, + start: Point, + cur: Point, + /// The page's size when the drag began. + size: (f64, f64), + alt: bool, +} + +/// The eight handles of a page rect: (reference-point index, canvas point). +fn page_handles(r: designcraft_geom::Rect) -> impl Iterator { + (0..9).filter(|i| *i != 4).map(move |i| (i, Point::new(r.x0 + r.width() * (i % 3) as f64 / 2.0, r.y0 + r.height() * (i / 3) as f64 / 2.0))) +} + +/// The liquid page rule as the Page tool tags a page. +pub fn rule_label(rule: designcraft_doc::LiquidRule) -> &'static str { + use designcraft_doc::LiquidRule as R; + match rule { + R::Off => "Off", + R::Scale => "Scale", + R::ReCenter => "Re-center", + R::ObjectBased => "Object-Based", + R::GuideBased => "Guide-Based", + } +} + +impl PageTool { + /// The selected page's handle under `p`. + fn handle_at(&self, cx: &ToolContext, p: Point) -> Option<(usize, usize)> { + let page = cx.selection.page?; + let r = cx.layout.page_rect(cx.doc, page)?; + page_handles(r).find(|(_, h)| (h.x - p.x).abs() <= cx.tol(5.0) && (h.y - p.y).abs() <= cx.tol(5.0)).map(|(i, _)| (page, i)) + } + + /// The new page size for the drag at `p`: the handle's edges follow the pointer (Shift: in + /// proportion). + fn size_at(cx: &ToolContext, d: &PageDrag, p: Point, shift: bool) -> (f64, f64) { + let si = cx.doc.page_loc(d.page).map_or(0, |l| l.0); + let v = cx.layout.delta_to_spread(designcraft_doc::SpreadRef::Doc(si), p - d.start); + let (col, row) = (d.handle % 3, d.handle / 3); + let dx = match col { + 0 => -v.x, + 2 => v.x, + _ => 0.0, + }; + let dy = match row { + 0 => -v.y, + 2 => v.y, + _ => 0.0, + }; + let (w0, h0) = d.size; + let (mut w, mut h) = ((w0 + dx).max(1.0), (h0 + dy).max(1.0)); + if shift { + // The axis being dragged (the larger change at a corner) sets the scale. + let k = match (col == 1, row == 1) { + (true, _) => h / h0, + (_, true) => w / w0, + _ if (w / w0 - 1.0).abs() >= (h / h0 - 1.0).abs() => w / w0, + _ => h / h0, + }; + (w, h) = ((w0 * k).max(1.0), (h0 * k).max(1.0)); + } + (w, h) + } +} impl Tool for PageTool { fn id(&self) -> &'static str { "page" } fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec { - if ev.kind != PointerKind::Up { - return vec![]; + match ev.kind { + PointerKind::Down => { + if let Some((page, handle)) = self.handle_at(cx, ev.pos) { + let Some((si, pi)) = cx.doc.page_loc(page) else { return vec![] }; + let pg = &cx.doc.spreads[si].pages[pi]; + self.drag = Some(PageDrag { page, handle, start: ev.pos, cur: ev.pos, size: (pg.width, pg.height), alt: ev.mods.alt }); + return vec![Action::Begin("Resize Page".into())]; + } + // A click selects the page under the pointer (none on the pasteboard). + let page = (0..cx.doc.page_count()).find(|p| cx.layout.page_rect(cx.doc, *p).is_some_and(|r| r.contains(ev.pos))); + if page == cx.selection.page { + return vec![]; + } + vec![Action::Exec("layout.selectPage".into(), json!({"page": page.map(|p| p + 1)}))] + } + PointerKind::Drag => { + let Some(d) = self.drag.as_mut() else { return vec![] }; + d.cur = ev.pos; + d.alt |= ev.mods.alt; + let d = *d; + let (w, h) = Self::size_at(cx, &d, ev.pos, ev.mods.shift); + vec![Action::Preview("layout.pageSize".into(), json!({"pages": [d.page + 1], "width": w, "height": h, "anchor": 8 - d.handle}))] + } + PointerKind::Up => match self.drag.take() { + // Alt-drag resizes; a plain drag previews the liquid rule and springs back. + Some(d) if d.alt || ev.mods.alt => vec![Action::Commit], + Some(_) => vec![Action::Cancel], + None => vec![], + }, + PointerKind::DoubleClick => { + // Exact sizes: the Page Size dialog for the page. + let Some(page) = (0..cx.doc.page_count()).find(|p| cx.layout.page_rect(cx.doc, *p).is_some_and(|r| r.contains(ev.pos))) else { + return vec![]; + }; + let Some((si, pi)) = cx.doc.page_loc(page) else { return vec![] }; + let pg = &cx.doc.spreads[si].pages[pi]; + vec![Action::Dialog("cmd:layout.pageSize".into(), json!({"pages": [page + 1], "width": pg.width, "height": pg.height}))] + } + PointerKind::Move => vec![], } - let Some((designcraft_doc::SpreadRef::Doc(si), sp)) = cx.layout.spread_at(ev.pos) else { return vec![] }; - let Some(spread) = cx.doc.spreads.get(si) else { return vec![] }; - let Some(pi) = spread.pages.iter().position(|p| p.bounds().contains(sp)) else { return vec![] }; - let pg = &spread.pages[pi]; - let n = cx.doc.first_page_of_spread(si) + pi + 1; - vec![Action::Dialog("cmd:layout.pageSize".into(), json!({"pages": [n], "width": pg.width, "height": pg.height}))] } - fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor { - Cursor::Arrow + fn overlays(&self, cx: &ToolContext) -> Vec { + let Some(page) = cx.selection.page else { return vec![] }; + let Some(r) = cx.layout.page_rect(cx.doc, page) else { return vec![] }; + let Some((si, pi)) = cx.doc.page_loc(page) else { return vec![] }; + let pg = &cx.doc.spreads[si].pages[pi]; + let mut v = vec![Overlay::PageHandles(r), Overlay::Tag { p: Point::new(r.x0, r.y0 - cx.tol(6.0)), text: rule_label(pg.liquid).into() }]; + if let Some(d) = self.drag { + v.push(Overlay::Measure { + p: d.cur, + text: format!( + "W: {}\nH: {}", + designcraft_geom::format_measure(pg.width, cx.unit), + designcraft_geom::format_measure(pg.height, cx.unit) + ), + }); + } + v + } + fn cursor(&self, cx: &ToolContext, p: Point, _m: Mods) -> Cursor { + let handle = self.drag.map(|d| d.handle).or_else(|| self.handle_at(cx, p).map(|(_, h)| h)); + match handle { + Some(1 | 7) => Cursor::ResizeV, + Some(3 | 5) => Cursor::ResizeH, + Some(0 | 8) => Cursor::ResizeNwSe, + Some(_) => Cursor::ResizeNeSw, + None => Cursor::Arrow, + } + } + fn busy(&self) -> bool { + self.drag.is_some() } } diff --git a/crates/tools/src/tests.rs b/crates/tools/src/tests.rs index 6776902aa..b2e597242 100644 --- a/crates/tools/src/tests.rs +++ b/crates/tools/src/tests.rs @@ -227,14 +227,22 @@ fn hand_tool_alt_press_power_zooms() { } #[test] -fn page_tool_opens_page_size() { +fn page_tool_selects_a_page_and_opens_page_size_on_double_click() { let d = Document::new(&NewDocument::default()); let (s, c, l) = (Selection::default(), Cache::new(), CanvasLayout::new(&d, false)); let cx = ctx(&d, &s, &c, &l); let off = l.xf(SpreadRef::Doc(0)).translation(); + let (x, y) = (200.0 + off.x + d.spreads[0].pages[0].x, 200.0 + off.y); let mut t = create("page"); - let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 200.0 + off.x + d.spreads[0].pages[0].x, 200.0 + off.y)); + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, x, y)); + assert_eq!(a, vec![Action::Exec("layout.selectPage".into(), serde_json::json!({"page": 1}))]); + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::DoubleClick, x, y)); assert_eq!(a, vec![Action::Dialog("cmd:layout.pageSize".into(), serde_json::json!({"pages": [1], "width": 612.0, "height": 792.0}))]); + // The selected page shows its handles and its liquid page rule. + let s = Selection { page: Some(0), ..Default::default() }; + let cx = ctx(&d, &s, &c, &l); + let o = t.overlays(&cx); + assert!(matches!(o.as_slice(), [Overlay::PageHandles(_), Overlay::Tag { text, .. }] if text == "Off"), "{o:?}"); } #[test] diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 077ae2944..b3fa92336 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1237,6 +1237,27 @@ fn draw_tool_overlays(app: &mut DesignApp, painter: &egui::Painter, xf: &Xf) { painter.rect_filled(r, 3.0, tok.measure_bg); painter.galley(r.min + vec2(5.0, 3.0), g, tok.measure_text); } + Overlay::PageHandles(r) => { + // The Page tool's selected page: outline and eight handles, as InDesign draws them. + let r = xf.rect(r); + let col = Color32::from_rgb(79, 153, 255); + painter.rect_stroke(r, 0.0, Stroke::new(1.0, col), StrokeKind::Middle); + for i in (0..9).filter(|i| *i != 4) { + let c = pos2(r.min.x + r.width() * (i % 3) as f32 / 2.0, r.min.y + r.height() * (i / 3) as f32 / 2.0); + let h = Rect::from_center_size(c, vec2(7.0, 7.0)); + painter.rect_filled(h, 0.0, Color32::WHITE); + painter.rect_stroke(h, 0.0, Stroke::new(1.0, col), StrokeKind::Inside); + } + } + Overlay::Tag { p, text } => { + let text = crate::i18n::tr(&app.ui.language, &text).to_owned(); + let s = xf.to_screen(p); + let g = painter.layout_no_wrap(text, egui::FontId::proportional(10.0), tok.measure_text); + let r = Rect::from_min_size(s - vec2(0.0, g.size().y + 4.0), g.size() + vec2(8.0, 4.0)); + painter.rect_filled(r, 2.0, tok.measure_bg); + painter.rect_stroke(r, 2.0, Stroke::new(1.0, Color32::from_rgb(79, 153, 255)), StrokeKind::Inside); + painter.galley(r.min + vec2(4.0, 2.0), g, tok.measure_text); + } Overlay::Path { .. } => {} } } diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 5252c3881..63c70d4a7 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -2498,6 +2498,9 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Added", ["Added", "Added", "Added", "Added", "已添加"]), ("Deleted", ["Deleted", "Deleted", "Deleted", "Deleted", "已删除"]), ("Guide-based", ["Guide-based", "Guide-based", "Guide-based", "Guide-based", "基于参考线"]), + ("Object-Based", ["Object-Based", "Object-Based", "Object-Based", "Object-Based", "基于对象"]), + ("Guide-Based", ["Guide-Based", "Guide-Based", "Guide-Based", "Guide-Based", "基于参考线"]), + ("Resize Page", ["Seitengröße ändern", "Redimensionner la page", "Cambiar tamaño de página", "ページのサイズを変更", "调整页面大小"]), ("Loop", ["Loop", "Loop", "Loop", "Loop", "循环播放"]), ("Object-based", ["Object-based", "Object-based", "Object-based", "Object-based", "基于对象"]), ("Off", ["Off", "Off", "Off", "Off", "关闭"]), diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index a182d09ec..e0c17c88a 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -709,6 +709,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Added", "مضاف"), ("Deleted", "محذوف"), ("Guide-based", "استنادًا إلى الأدلة"), + ("Object-Based", "استنادًا إلى الكائنات"), + ("Guide-Based", "استنادًا إلى الأدلة"), + ("Resize Page", "تغيير حجم الصفحة"), ("Loop", "تكرار"), ("Object-based", "استنادًا إلى الكائنات"), ("Play on Page Load", "تشغيل عند تحميل الصفحة"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 75ece03f6..3f0915aa3 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -1295,6 +1295,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Added", "Aggiunto"), ("Deleted", "Eliminato"), ("Guide-based", "Basato su guide"), + ("Object-Based", "Basato su oggetti"), + ("Guide-Based", "Basato su guide"), + ("Resize Page", "Ridimensiona pagina"), ("Loop", "Ciclo continuo"), ("Object-based", "Basato su oggetti"), ("Off", "Disattivato"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 642b4a4c0..9e0f7a953 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -1291,6 +1291,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Added", "追加済み"), ("Deleted", "削除済み"), ("Guide-based", "ガイド基準"), + ("Object-Based", "オブジェクト基準"), + ("Guide-Based", "ガイド基準"), + ("Resize Page", "ページのサイズを変更"), ("Loop", "繰り返し再生"), ("Object-based", "オブジェクト基準"), ("Off", "オフ"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 982dfb20d..e1fbf308a 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -708,6 +708,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Added", "Adicionado"), ("Deleted", "Excluído"), ("Guide-based", "Baseado em guias"), + ("Object-Based", "Baseado em objetos"), + ("Guide-Based", "Baseado em guias"), + ("Resize Page", "Redimensionar página"), ("Loop", "Repetir"), ("Object-based", "Baseado em objetos"), ("Play on Page Load", "Reproduzir ao carregar a página"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index f3bc8fb35..5994dc135 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -1292,6 +1292,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Added", "Додано"), ("Deleted", "Видалено"), ("Guide-based", "На основі напрямних"), + ("Object-Based", "На основі об’єктів"), + ("Guide-Based", "На основі напрямних"), + ("Resize Page", "Змінити розмір сторінки"), ("Loop", "Зациклити"), ("Object-based", "На основі об’єктів"), ("Off", "Вимкнено"), diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index eb7d7e5c1..84b602d54 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -396,7 +396,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level - Type on a Path (⇧T tool, `type.onPath`): a story along any line, curve or shape on screen and in PDF; Type on a Path Options (start, flip, baseline/ascender/descender/centre) and Delete Type from Path. Round-trips through IDML as TextPath; on-path caret drawing is still to come. - Preferences › Appearance of Black: 100% K on screen accurately (dark grey) or as rich black (`window.richBlack`); the printing/export side is still to come. - Spreads of up to 10 pages: Add to Previous Spread and Allow Spread to Shuffle (Pages panel; `layout.pages.toSpread`, `layout.spreadShuffle`); inserted pages go around kept spreads. -- Page tool (⇧P) and `layout.pageSize`: pages of their own size (or a preset); the spine stays put and objects keep their place on their page. +- Page tool (⇧P) and `layout.pageSize`: pages of their own size (or a preset); the spine stays put and objects keep their place on their page. A click selects a page (handles, its liquid page rule tagged above it); dragging a handle previews the liquid rule and springs back, Alt-drag resizes, Shift keeps proportions, with a W/H readout; `anchor` keeps a reference point in place; double-click for exact sizes. - File › Print (⌘P): printer, copies, page range, spreads, marks, bleed; printed as PDF through the system spooler (`file.print`, `file.printers`, `dryRun` for agents). Shortcuts now act like choosing the menu item (dialogs open), and UI commands win shortcut ties (⌘N opens New Document). - Nested styles (through/up to N sentences, words, characters, letters, digits, tabs, breaks, spaces or given characters) and GREP styles (regex → character style), edited in Paragraph Style Options and composed under local formatting; `ui.dialog.open` for agents. Nested line styles are still to come; nested and GREP styles round-trip through IDML. - Table and cell styles (round-trip through IDML): cell styles (fill, insets, vertical justification, strokes, paragraph style) and table styles (region cell styles, border, alternating rows, spacing); apply from the Table panel, edits re-apply to every user; `style.cell.*`, `style.table.*`.