mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
Merge PR #862
This commit is contained in:
commit
c1a1970168
15 files changed
+340
-21
No files matched your search
@@ -134,6 +134,7 @@ Newest first.
|
||||
- **2026-10-13** · Object › Fitting has InDesign's shortcuts: Fill Frame Proportionally ⌘⌥⇧C, Fit Content Proportionally ⌘⌥⇧E, Fit Frame to Content ⌘⌥C, Fit Content to Frame ⌘⌥E, Center Content ⌘⇧E. No readiness percentage change.
|
||||
- **2026-10-13** · Gradients between process colours blend as inks, as InDesign shows them: magenta to cyan passes through C 50 M 50 (a light lilac) on screen instead of the darker RGB mix, and a midpoint between CMYK or grey stops no longer turns the PDF shading RGB. No readiness percentage change.
|
||||
- **2026-10-14** · The Swatches panel has InDesign's header: a Fill/Stroke proxy, Formatting Affects Container/Text and the Tint of the colour in front (a tint alone keeps each object's own swatch). Clicking a swatch applies it to the fill or stroke in front (of the text, when formatting affects text), and the bottom row has New Color Group, New Swatch and Delete Swatch. 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-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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,6 +570,79 @@ fn commands_take_non_object_params_without_panicking() {
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
@@ -322,6 +322,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`.
|
||||
@@ -412,7 +420,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]
|
||||
|
||||
@@ -1301,6 +1301,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 { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2516,6 +2516,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", "关闭"]),
|
||||
|
||||
@@ -718,6 +718,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "مضاف"),
|
||||
("Deleted", "محذوف"),
|
||||
("Guide-based", "استنادًا إلى الأدلة"),
|
||||
("Object-Based", "استنادًا إلى الكائنات"),
|
||||
("Guide-Based", "استنادًا إلى الأدلة"),
|
||||
("Resize Page", "تغيير حجم الصفحة"),
|
||||
("Loop", "تكرار"),
|
||||
("Object-based", "استنادًا إلى الكائنات"),
|
||||
("Play on Page Load", "تشغيل عند تحميل الصفحة"),
|
||||
|
||||
@@ -1296,6 +1296,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"),
|
||||
|
||||
@@ -1292,6 +1292,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "追加済み"),
|
||||
("Deleted", "削除済み"),
|
||||
("Guide-based", "ガイド基準"),
|
||||
("Object-Based", "オブジェクト基準"),
|
||||
("Guide-Based", "ガイド基準"),
|
||||
("Resize Page", "ページのサイズを変更"),
|
||||
("Loop", "繰り返し再生"),
|
||||
("Object-based", "オブジェクト基準"),
|
||||
("Off", "オフ"),
|
||||
|
||||
@@ -709,6 +709,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"),
|
||||
|
||||
@@ -1293,6 +1293,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "Додано"),
|
||||
("Deleted", "Видалено"),
|
||||
("Guide-based", "На основі напрямних"),
|
||||
("Object-Based", "На основі об’єктів"),
|
||||
("Guide-Based", "На основі напрямних"),
|
||||
("Resize Page", "Змінити розмір сторінки"),
|
||||
("Loop", "Зациклити"),
|
||||
("Object-based", "На основі об’єктів"),
|
||||
("Off", "Вимкнено"),
|
||||
|
||||
@@ -398,7 +398,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