mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
feat(tools): Page tool selects pages; handles preview the liquid rule, Alt-drag resizes
Chris Cicchino's "Adobe InDesign – Page Tool and Liquid Layout" (0:42–
2:06) selects a page with the Page tool, gets its bounding box with
handles, drags a handle (the page springs back: a liquid-rule preview),
holds Option to really resize it, Shift for proportions, and reads the
size in a tooltip by the cursor. DesignCraft's Page tool only opened a
generic Page Size dialog on click.
- tools: the Page tool selects the page under a click (none on the
pasteboard); the selected page shows its outline, eight handles and
its liquid page rule ("Scale", "Object-Based"…) above it. Dragging a
handle previews layout.pageSize (the liquid rule re-flows the
objects) and springs back on release; Alt resizes, Shift keeps the
proportions; a W/H readout follows the pointer; resize cursors on the
handles. Double-click a page for the Page Size dialog.
- doc: Selection.page (the Page tool's page); engine: layout.selectPage
(no undo step).
- layout.pageSize `anchor` (0-8): the page grows away from the
reference point and its objects and guides keep their place relative
to it (a handle drag anchors the opposite edge); liquid rules work
from that.
- canvas: PageHandles and Tag overlays; translations for the tags and
the Resize Page undo label.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e862e0a613
commit
cc13e80c9d
15 files changed
+340
-21
No files matched your search
@@ -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.
|
||||
|
||||
@@ -80,6 +80,9 @@ pub struct Selection {
|
||||
/// Key object for Align.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub key: Option<ItemId>,
|
||||
/// The page selected with the Page tool (absolute index).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub page: Option<usize>,
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
|
||||
@@ -376,10 +376,24 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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<Value> {
|
||||
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<Value> {
|
||||
/// 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<f64>, h: Option<f64>) {
|
||||
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<f64>, h: Option<f64>, 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<f64> = 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<usize> = 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<dyn Tool> {
|
||||
"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)),
|
||||
|
||||
+147
-13
@@ -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<PageDrag>,
|
||||
}
|
||||
|
||||
/// 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<Item = (usize, Point)> {
|
||||
(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<Action> {
|
||||
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<Overlay> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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", "关闭"]),
|
||||
|
||||
@@ -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", "تشغيل عند تحميل الصفحة"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -1291,6 +1291,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "追加済み"),
|
||||
("Deleted", "削除済み"),
|
||||
("Guide-based", "ガイド基準"),
|
||||
("Object-Based", "オブジェクト基準"),
|
||||
("Guide-Based", "ガイド基準"),
|
||||
("Resize Page", "ページのサイズを変更"),
|
||||
("Loop", "繰り返し再生"),
|
||||
("Object-based", "オブジェクト基準"),
|
||||
("Off", "オフ"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -1292,6 +1292,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "Додано"),
|
||||
("Deleted", "Видалено"),
|
||||
("Guide-based", "На основі напрямних"),
|
||||
("Object-Based", "На основі об’єктів"),
|
||||
("Guide-Based", "На основі напрямних"),
|
||||
("Resize Page", "Змінити розмір сторінки"),
|
||||
("Loop", "Зациклити"),
|
||||
("Object-based", "На основі об’єктів"),
|
||||
("Off", "Вимкнено"),
|
||||
|
||||
@@ -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.*`.
|
||||
|
||||
Reference in new issue
Block a user