mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 03:06:56 -06:00
@@ -128,6 +128,11 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-14** · The Liquid Layout panel follows InDesign's: Liquid Page Rule with the page number, Content › Auto-Fit, and Object Constraints › Resize with Page and Pin for the selected object on an object-based page; Create Alternate Layout moved to the Layout menu. No readiness percentage change.
|
||||
- **2026-10-14** · On an object-based page the Page tool shows a selected object's liquid pins and resize locks as InDesign does: a click pins a side to the page edge or locks the width or height, with InDesign's tooltips. No readiness percentage change.
|
||||
- **2026-10-14** · With the Page tool, the Control panel shows the page as InDesign's does: W/H from a reference point, page-size preset (US Business Card added), orientation, Liquid Page Rule and Show Parent Page Overlay, which draws the parent page's size over a resized page. No readiness percentage change.
|
||||
- **2026-10-14** · The Page tool works as InDesign's: a click selects a page and shows its handles and liquid page rule; dragging a handle previews the rule and springs back, Alt-drag resizes the page from the opposite edge, Shift keeps proportions, with a W/H readout. No readiness percentage change.
|
||||
- **2026-10-14** · With the Page tool, a guide dragged from a ruler is a liquid guide (dashed), as in InDesign; guide-based pages stretch the objects it crosses. No readiness percentage change.
|
||||
- **2026-10-13** · Control-channel clicks and drags validate positive counts and the complete pending-event budget before queuing input (#732). Oversized requests leave pending gestures intact, preventing unbounded click-count allocation. No readiness percentage change.
|
||||
- **2026-10-13** · Switching between document and parent pages refreshes the canvas immediately, preventing black parent pages and stale document objects when the zoom stays the same. Ordinary edits retain partial repainting. No readiness percentage change.
|
||||
- **2026-10-13** · Placed PDFs are drawn through pdfcraft's patched hayro crates: calculator functions whose `idiv` divides by zero or overflows, chains of stitching functions a hundred thousand deep, and mesh shadings with points far off the page, ten thousand overlapping triangles or forty thousand patches no longer panic, overflow the stack, run for over a minute or use more than 2 GiB of memory. A page with a `/UserUnit`, and its trim, bleed and art boxes, are sized in points. No readiness percentage change.
|
||||
|
||||
@@ -227,6 +227,7 @@ impl Document {
|
||||
liquid: Default::default(),
|
||||
transition: None,
|
||||
view_rotation: 0,
|
||||
show_parent_overlay: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,9 @@ pub struct Page {
|
||||
/// Liquid page rule: how objects follow when the page changes size.
|
||||
#[serde(default, skip_serializing_if = "LiquidRule::is_off")]
|
||||
pub liquid: LiquidRule,
|
||||
/// Page tool › Show Parent Page Overlay: the parent page's size drawn over this page.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub show_parent_overlay: bool,
|
||||
/// Window › Interactive › Page Transitions (the spread's first page holds the spread's).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub transition: Option<PageTransition>,
|
||||
@@ -212,6 +215,8 @@ pub enum LiquidRule {
|
||||
GuideBased,
|
||||
/// Each object's pins and resize settings decide (`Item::liquid`).
|
||||
ObjectBased,
|
||||
/// The rule of the page's parent page (InDesign's "Controlled by Parent"; Off without one).
|
||||
ControlledByParent,
|
||||
}
|
||||
|
||||
impl LiquidRule {
|
||||
@@ -396,6 +401,7 @@ mod tests {
|
||||
liquid: Default::default(),
|
||||
transition: None,
|
||||
view_rotation: 0,
|
||||
show_parent_overlay: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"New Guide",
|
||||
[],
|
||||
None,
|
||||
"{orientation: horizontal|vertical, position (spread coordinate), spread?, page? (index in the spread; default: the page under `at` or the position), at? (the other coordinate), spreadGuide?: bool} or {orientation: both, point: [x, y], spread?, page?, spreadGuide?: bool} → {page, index}; `both` adds a horizontal guide through y and a vertical guide through x as one undo step → {guides: [{page, index}, {page, index}]}",
|
||||
"{orientation: horizontal|vertical, position (spread coordinate), spread?, page? (index in the spread; default: the page under `at` or the position), at? (the other coordinate), spreadGuide?: bool, liquid?: bool (a liquid guide, as the Page tool draws from the rulers)} or {orientation: both, point: [x, y], spread?, page?, spreadGuide?: bool, liquid?: bool} → {page, index}; `both` adds a horizontal guide through y and a vertical guide through x as one undo step → {guides: [{page, index}, {page, index}]}",
|
||||
has_doc,
|
||||
add
|
||||
),
|
||||
@@ -96,6 +96,7 @@ fn page_at(d: &Document, r: SpreadRef, x: f64) -> usize {
|
||||
fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let r = super::spread_param(p, "guide.add")?;
|
||||
let spread_guide = p.get("spreadGuide").and_then(Value::as_bool).unwrap_or(false);
|
||||
let liquid = p.get("liquid").and_then(Value::as_bool).unwrap_or(false);
|
||||
let page = optional_usize(p, "page", "guide.add")?;
|
||||
// (orientation, position, x used to find the page)
|
||||
let guides: Vec<(Orientation, f64, f64)> = if str_param(p, "orientation") == Some("both") {
|
||||
@@ -132,7 +133,14 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut out = Vec::new();
|
||||
for (o, pos, pi) in placed {
|
||||
let pg = sp.pages.get_mut(pi).ok_or_else(|| bad("guide.add", "no such page"))?;
|
||||
pg.guides.push(Guide { orientation: o, position: pos, spread: spread_guide, locked: false, layer: Some(layer), liquid: false });
|
||||
pg.guides.push(Guide {
|
||||
orientation: o,
|
||||
position: pos,
|
||||
spread: spread_guide,
|
||||
locked: false,
|
||||
layer: Some(layer),
|
||||
liquid: liquid && !spread_guide,
|
||||
});
|
||||
out.push(json!({"page": pi, "index": pg.guides.len() - 1}));
|
||||
}
|
||||
match <[Value; 1]>::try_from(out) {
|
||||
|
||||
@@ -465,10 +465,48 @@ 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!(
|
||||
"layout.pageOptions",
|
||||
"Page Options",
|
||||
[],
|
||||
None,
|
||||
"{pages: [1-based], showParentOverlay?: bool} — Page tool options (Show Parent Page Overlay: the parent page's size drawn over the page)",
|
||||
has_doc,
|
||||
page_options
|
||||
),
|
||||
cmd!(
|
||||
noundo "layout.selectPage",
|
||||
"Select Page",
|
||||
[],
|
||||
None,
|
||||
"{page: 1-based | null, ids?: [item ids] (objects selected with it: the Page tool on an object-based page)} — 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 ids: Vec<designcraft_doc::ItemId> = {
|
||||
let d = &s.doc()?.doc;
|
||||
p.get("ids")
|
||||
.and_then(Value::as_array)
|
||||
.map(|a| a.iter().filter_map(Value::as_u64).map(designcraft_doc::ItemId).filter(|id| d.item(*id).is_some()).collect())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let st = s.doc_mut()?;
|
||||
if page.is_some() {
|
||||
st.selection = designcraft_doc::Selection::items(ids);
|
||||
}
|
||||
st.selection.page = page;
|
||||
st.revision += 1;
|
||||
Ok(json!({"page": page.map(|p| p + 1)}))
|
||||
}
|
||||
),
|
||||
cmd!(
|
||||
"layout.section",
|
||||
"Numbering & Section Options…",
|
||||
@@ -793,8 +831,32 @@ 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()}))
|
||||
})
|
||||
}
|
||||
|
||||
fn page_options(s: &mut crate::Session, p: &Value) -> Result<Value> {
|
||||
let pages: Vec<usize> =
|
||||
p.get("pages").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(|n| n as usize).collect()).unwrap_or_default();
|
||||
let n = s.doc()?.doc.page_count();
|
||||
if pages.is_empty() || pages.iter().any(|x| *x == 0 || *x > n) {
|
||||
return Err(bad("layout.pageOptions", "give `pages` (1-based)"));
|
||||
}
|
||||
let overlay = p.get("showParentOverlay").and_then(Value::as_bool);
|
||||
s.edit(|d, _| {
|
||||
for page in &pages {
|
||||
let Some((si, pi)) = d.page_loc(page - 1) else { continue };
|
||||
let pg = &mut std::sync::Arc::make_mut(&mut d.spreads[si]).pages[pi];
|
||||
if let Some(v) = overlay {
|
||||
pg.show_parent_overlay = v;
|
||||
}
|
||||
}
|
||||
Ok(json!({"pages": pages.len()}))
|
||||
})
|
||||
}
|
||||
@@ -802,14 +864,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);
|
||||
}
|
||||
@@ -846,8 +929,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ fn axis(lo: f64, hi: f64, o: (f64, f64), n: (f64, f64), pin_lo: bool, pin_hi: bo
|
||||
fn target(rule: LiquidRule, b: Rect, old: Rect, new: Rect, liquid: Option<ObjectLiquid>, vguides: &[f64], hguides: &[f64]) -> Rect {
|
||||
let (ox, oy, nx, ny) = ((old.x0, old.x1), (old.y0, old.y1), (new.x0, new.x1), (new.y0, new.y1));
|
||||
match rule {
|
||||
LiquidRule::Off => b,
|
||||
LiquidRule::Off | LiquidRule::ControlledByParent => b,
|
||||
LiquidRule::ReCenter => b + (new.center() - old.center()),
|
||||
LiquidRule::Scale => {
|
||||
let k = (new.width() / old.width()).min(new.height() / old.height());
|
||||
@@ -63,7 +63,7 @@ fn target(rule: LiquidRule, b: Rect, old: Rect, new: Rect, liquid: Option<Object
|
||||
pub(crate) fn apply(d: &mut Document, sr: SpreadRef, pi: usize, old: Rect) {
|
||||
let Some(sp) = d.spread(sr) else { return };
|
||||
let Some(pg) = sp.pages.get(pi) else { return };
|
||||
let (rule, new) = (pg.liquid, pg.bounds());
|
||||
let (rule, new) = (effective_rule(d, sr, pi), pg.bounds());
|
||||
if rule == LiquidRule::Off || old == new || old.width() < 1e-6 || old.height() < 1e-6 {
|
||||
return;
|
||||
}
|
||||
@@ -108,8 +108,24 @@ pub(crate) fn apply(d: &mut Document, sr: SpreadRef, pi: usize, old: Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The rule page `pi` of spread `sr` follows: its own, or its parent page's when it's controlled
|
||||
/// by the parent (Off when that's unset too).
|
||||
pub(crate) fn effective_rule(d: &Document, sr: SpreadRef, pi: usize) -> LiquidRule {
|
||||
let Some(pg) = d.spread(sr).and_then(|sp| sp.pages.get(pi)) else { return LiquidRule::Off };
|
||||
if pg.liquid != LiquidRule::ControlledByParent {
|
||||
return pg.liquid;
|
||||
}
|
||||
let SpreadRef::Doc(si) = sr else { return LiquidRule::Off };
|
||||
let abs = d.first_page_of_spread(si) + pi;
|
||||
match d.parent_page_for(abs).and_then(|(ppi, ppg)| d.parents.get(ppi)?.pages.get(ppg).map(|p| p.liquid)) {
|
||||
Some(LiquidRule::ControlledByParent) | None => LiquidRule::Off,
|
||||
Some(r) => r,
|
||||
}
|
||||
}
|
||||
|
||||
fn rule_param(p: &Value, id: &str) -> Result<LiquidRule> {
|
||||
serde_json::from_value(p.get("rule").cloned().unwrap_or(json!("off"))).map_err(|_| bad(id, "`rule`: off|scale|reCenter|guideBased|objectBased"))
|
||||
serde_json::from_value(p.get("rule").cloned().unwrap_or(json!("off")))
|
||||
.map_err(|_| bad(id, "`rule`: off|scale|reCenter|guideBased|objectBased|controlledByParent"))
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
@@ -119,7 +135,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Liquid Page Rule",
|
||||
["Layout", "Liquid Layout"],
|
||||
None,
|
||||
"{rule: off|scale|reCenter|guideBased|objectBased, pages?: [1-based] (default: all)}",
|
||||
"{rule: off|scale|reCenter|guideBased|objectBased|controlledByParent (the parent page's rule), pages?: [1-based] (default: all)}",
|
||||
has_doc,
|
||||
|s, p| {
|
||||
let rule = rule_param(p, "liquid.pageRule")?;
|
||||
|
||||
@@ -1258,3 +1258,157 @@ fn alt_arrows_kern_at_the_caret_and_track_a_selection() {
|
||||
let t = s.doc().unwrap().selection.text.unwrap();
|
||||
assert_eq!((t.anchor, t.focus), (0, 5));
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controlled_by_parent_takes_the_parent_pages_rule() {
|
||||
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();
|
||||
// The page follows its parent; the parent page re-centres.
|
||||
s.execute("liquid.pageRule", &json!({"rule": "controlledByParent", "pages": [1]})).unwrap();
|
||||
s.edit(|d, _| {
|
||||
std::sync::Arc::make_mut(&mut d.parents[0]).pages[0].liquid = designcraft_doc::LiquidRule::ReCenter;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("layout.pageSize", &json!({"pages": [1], "width": 600})).unwrap();
|
||||
assert_eq!(frame(&s).x0, 200.0, "re-centred: {:?}", frame(&s));
|
||||
// Without a parent (or a parent that's also controlled by its parent) nothing moves.
|
||||
s.execute("layout.pages.applyParent", &json!({"pages": [0], "parent": null})).unwrap();
|
||||
s.execute("layout.pageSize", &json!({"pages": [1], "width": 800})).unwrap();
|
||||
assert_eq!(frame(&s).x0, 200.0);
|
||||
// Show Parent Page Overlay is a page option.
|
||||
s.execute("layout.pageOptions", &json!({"pages": [1], "showParentOverlay": true})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.spreads[0].pages[0].show_parent_overlay);
|
||||
assert!(s.execute("layout.pageOptions", &json!({"pages": [2], "showParentOverlay": true})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_tool_pins_and_locks_objects_on_object_based_pages() {
|
||||
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": [256, 72, 356, 132]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("liquid.pageRule", &json!({"rule": "objectBased"})).unwrap();
|
||||
s.execute("edit.deselectAll", &json!({})).unwrap();
|
||||
s.set_tool("page");
|
||||
let v = ViewInfo::at_zoom(1.0);
|
||||
let ev =
|
||||
|kind, p: designcraft_geom::Point, alt: bool| PointerEvent { mods: Mods { alt, ..Default::default() }, ..PointerEvent::new(kind, p.x, p.y) };
|
||||
let canvas = |s: &Session| {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let l = CanvasLayout::new(d, false);
|
||||
(
|
||||
l.page_rect(d, 0).unwrap(),
|
||||
l.xf(designcraft_doc::SpreadRef::Doc(0)).transform_rect_bbox(d.item(designcraft_doc::ItemId(id)).unwrap().bounds()),
|
||||
)
|
||||
};
|
||||
let click = |s: &mut Session, p: designcraft_geom::Point| {
|
||||
s.pointer(&ev(PointerKind::Down, p, false), v).unwrap();
|
||||
s.pointer(&ev(PointerKind::Up, p, false), v).unwrap();
|
||||
};
|
||||
let liquid = |s: &Session| s.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().liquid;
|
||||
// A click on the object selects it with its page; its pins and locks show.
|
||||
let (_, o) = canvas(&s);
|
||||
click(&mut s, o.center());
|
||||
let sel = s.doc().unwrap().selection.clone();
|
||||
assert_eq!((sel.page, sel.items.clone()), (Some(0), vec![designcraft_doc::ItemId(id)]));
|
||||
assert!(
|
||||
s.overlays(v)
|
||||
.iter()
|
||||
.any(|o| matches!(o, designcraft_tools::Overlay::LiquidPins { pins: [false, false, false, false], resize: [true, true], .. }))
|
||||
);
|
||||
// The right pin (14 px outside the right edge) and the width lock (10 px right of the centre).
|
||||
click(&mut s, designcraft_geom::Point::new(o.x1 + 14.0, o.center().y));
|
||||
click(&mut s, designcraft_geom::Point::new(o.center().x + 10.0, o.center().y));
|
||||
let l = liquid(&s).unwrap();
|
||||
assert!(l.pin_right && !l.resize_width && l.resize_height && !l.pin_left, "{l:?}");
|
||||
// Alt-drag the page 100 pt wider: the pinned, width-locked frame keeps its size by the right edge.
|
||||
let (p, _) = canvas(&s);
|
||||
let right = designcraft_geom::Point::new(p.x1, p.center().y);
|
||||
s.pointer(&ev(PointerKind::Down, right, true), v).unwrap();
|
||||
s.pointer(&ev(PointerKind::Drag, right + designcraft_geom::Vec2::new(100.0, 0.0), true), v).unwrap();
|
||||
s.pointer(&ev(PointerKind::Up, right + designcraft_geom::Vec2::new(100.0, 0.0), true), v).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let (pg, b) = (&d.spreads[0].pages[0], d.item(designcraft_doc::ItemId(id)).unwrap().bounds());
|
||||
assert_eq!(pg.width, 712.0);
|
||||
assert!((b.width() - 100.0).abs() < 1e-6 && (pg.x + pg.width - b.x1 - 256.0).abs() < 1e-6, "{b:?}");
|
||||
// Hovering a pin shows its tooltip.
|
||||
let (_, o) = canvas(&s);
|
||||
s.pointer(&ev(PointerKind::Move, designcraft_geom::Point::new(o.x1 + 14.0, o.center().y), false), v).unwrap();
|
||||
assert!(s.overlays(v).iter().any(|o| matches!(o, designcraft_tools::Overlay::Tip { text, .. } if text.starts_with("Object is pinned"))));
|
||||
}
|
||||
|
||||
@@ -2423,6 +2423,7 @@ impl<'r> Importer<'r> {
|
||||
liquid: Default::default(),
|
||||
transition: None,
|
||||
view_rotation: if i == 0 { view_rotation } else { 0 },
|
||||
show_parent_overlay: false,
|
||||
});
|
||||
}
|
||||
let overrides: Vec<Vec<String>> =
|
||||
|
||||
+24
-1
@@ -16,6 +16,7 @@ mod frame;
|
||||
mod gradient;
|
||||
pub mod layout;
|
||||
mod nav;
|
||||
pub use nav::{LOCK_OFFSET_PX, PIN_OFFSET_PX};
|
||||
mod pen;
|
||||
mod pencil;
|
||||
pub mod select;
|
||||
@@ -352,6 +353,28 @@ pub enum Overlay {
|
||||
LiveCornerWidget(Point),
|
||||
/// A live-corner diamond: dragging it sets the corner size.
|
||||
LiveCornerDiamond(Point),
|
||||
/// 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,
|
||||
},
|
||||
/// A tooltip near the pointer (English; the UI translates it).
|
||||
Tip {
|
||||
p: Point,
|
||||
text: String,
|
||||
},
|
||||
/// An object's liquid pins and resize locks on an object-based page (Page tool), canvas
|
||||
/// coordinates: pinned sides (top, right, bottom, left) and whether width and height follow the
|
||||
/// page.
|
||||
LiquidPins {
|
||||
object: Rect,
|
||||
page: Rect,
|
||||
pins: [bool; 4],
|
||||
resize: [bool; 2],
|
||||
},
|
||||
}
|
||||
|
||||
/// Correction from a snap, in spread coordinates for `delta`.
|
||||
@@ -474,7 +497,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)),
|
||||
|
||||
+279
-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.
|
||||
@@ -169,27 +169,293 @@ 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>,
|
||||
/// The pin or lock under the pointer (for its tooltip).
|
||||
hover: Option<(LiquidControl, Point)>,
|
||||
}
|
||||
|
||||
/// An object's liquid control on an object-based page: a side's pin (top, right, bottom, left) or
|
||||
/// the width/height lock.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum LiquidControl {
|
||||
Pin(usize),
|
||||
Width,
|
||||
Height,
|
||||
}
|
||||
|
||||
impl LiquidControl {
|
||||
/// The `liquid.object` parameter it toggles.
|
||||
fn key(self) -> &'static str {
|
||||
match self {
|
||||
LiquidControl::Pin(0) => "pinTop",
|
||||
LiquidControl::Pin(1) => "pinRight",
|
||||
LiquidControl::Pin(2) => "pinBottom",
|
||||
LiquidControl::Pin(_) => "pinLeft",
|
||||
LiquidControl::Width => "resizeWidth",
|
||||
LiquidControl::Height => "resizeHeight",
|
||||
}
|
||||
}
|
||||
|
||||
/// InDesign's tooltip for the control in its current state.
|
||||
fn tip(self, l: &designcraft_doc::ObjectLiquid) -> &'static str {
|
||||
match self {
|
||||
LiquidControl::Pin(i) if [l.pin_top, l.pin_right, l.pin_bottom, l.pin_left][i.min(3)] => {
|
||||
"Object is pinned relative to the page edge. Click to release."
|
||||
}
|
||||
LiquidControl::Pin(_) => "Click to pin the object relative to the page edge.",
|
||||
LiquidControl::Width if l.resize_width => "The object changes width as the page changes width. Click to lock the object width.",
|
||||
LiquidControl::Width => "The object's width is locked. Click to let the object change width as the page changes width.",
|
||||
LiquidControl::Height if l.resize_height => "The object changes height as the page changes height. Click to lock the object height.",
|
||||
LiquidControl::Height => "The object's height is locked. Click to let the object change height as the page changes height.",
|
||||
}
|
||||
}
|
||||
|
||||
fn value(self, l: &designcraft_doc::ObjectLiquid) -> bool {
|
||||
match self {
|
||||
LiquidControl::Pin(0) => l.pin_top,
|
||||
LiquidControl::Pin(1) => l.pin_right,
|
||||
LiquidControl::Pin(2) => l.pin_bottom,
|
||||
LiquidControl::Pin(_) => l.pin_left,
|
||||
LiquidControl::Width => l.resize_width,
|
||||
LiquidControl::Height => l.resize_height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Screen pixels between an object's edge and its pin (the canvas draws them there too).
|
||||
pub const PIN_OFFSET_PX: f64 = 14.0;
|
||||
/// Screen pixels between an object's centre and its width/height locks.
|
||||
pub const LOCK_OFFSET_PX: f64 = 10.0;
|
||||
|
||||
/// Where an object's liquid controls sit (canvas): pins outside each edge's middle, the width lock
|
||||
/// right of the centre, the height lock above it.
|
||||
fn liquid_control_points(cx: &ToolContext, r: designcraft_geom::Rect) -> [(LiquidControl, Point); 6] {
|
||||
let (c, o, k) = (r.center(), cx.tol(PIN_OFFSET_PX), cx.tol(LOCK_OFFSET_PX));
|
||||
[
|
||||
(LiquidControl::Pin(0), Point::new(c.x, r.y0 - o)),
|
||||
(LiquidControl::Pin(1), Point::new(r.x1 + o, c.y)),
|
||||
(LiquidControl::Pin(2), Point::new(c.x, r.y1 + o)),
|
||||
(LiquidControl::Pin(3), Point::new(r.x0 - o, c.y)),
|
||||
(LiquidControl::Width, Point::new(c.x + k, c.y)),
|
||||
(LiquidControl::Height, Point::new(c.x, c.y - k)),
|
||||
]
|
||||
}
|
||||
|
||||
/// 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",
|
||||
R::ControlledByParent => "Controlled by Parent",
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether page `abs` follows the object-based rule (its own, or its parent page's).
|
||||
fn object_based(doc: &designcraft_doc::Document, abs: usize) -> bool {
|
||||
use designcraft_doc::LiquidRule as R;
|
||||
let Some((si, pi)) = doc.page_loc(abs) else { return false };
|
||||
match doc.spreads[si].pages[pi].liquid {
|
||||
R::ObjectBased => true,
|
||||
R::ControlledByParent => {
|
||||
doc.parent_page_for(abs).and_then(|(ppi, ppg)| doc.parents.get(ppi)?.pages.get(ppg).map(|p| p.liquid == R::ObjectBased)).unwrap_or(false)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
impl PageTool {
|
||||
/// The selected object's liquid settings, canvas bounds and page bounds when the Page tool
|
||||
/// shows its pins: one object selected on the selected page, whose rule is object-based.
|
||||
fn liquid_target(
|
||||
cx: &ToolContext,
|
||||
) -> Option<(designcraft_doc::ItemId, designcraft_doc::ObjectLiquid, designcraft_geom::Rect, designcraft_geom::Rect)> {
|
||||
let page = cx.selection.page?;
|
||||
let [id] = cx.selection.items.as_slice() else { return None };
|
||||
let (si, _) = cx.doc.page_loc(page)?;
|
||||
if !object_based(cx.doc, page) || cx.doc.page_of_item(*id) != Some(page) {
|
||||
return None;
|
||||
}
|
||||
let it = cx.doc.item(*id)?;
|
||||
let l = it.liquid.unwrap_or(designcraft_doc::ObjectLiquid { resize_width: true, resize_height: true, ..Default::default() });
|
||||
let r = cx.layout.xf(designcraft_doc::SpreadRef::Doc(si)).transform_rect_bbox(it.bounds());
|
||||
Some((*id, l, r, cx.layout.page_rect(cx.doc, page)?))
|
||||
}
|
||||
|
||||
/// The selected object's pin or lock under `p`.
|
||||
fn control_at(cx: &ToolContext, p: Point) -> Option<(designcraft_doc::ItemId, designcraft_doc::ObjectLiquid, LiquidControl)> {
|
||||
let (id, l, r, _) = Self::liquid_target(cx)?;
|
||||
liquid_control_points(cx, r).into_iter().find(|(_, q)| (q.x - p.x).hypot(q.y - p.y) <= cx.tol(6.0)).map(|(c, _)| (id, l, c))
|
||||
}
|
||||
|
||||
/// 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())];
|
||||
}
|
||||
// An object-based page's object: a click on a pin or lock toggles it.
|
||||
if let Some((id, l, c)) = Self::control_at(cx, ev.pos) {
|
||||
return vec![Action::Exec("liquid.object".into(), json!({"ids": [id.0], c.key(): !c.value(&l)}))];
|
||||
}
|
||||
// A click selects the page under the pointer (none on the pasteboard), and on an
|
||||
// object-based page the object clicked too.
|
||||
let page = (0..cx.doc.page_count()).find(|p| cx.layout.page_rect(cx.doc, *p).is_some_and(|r| r.contains(ev.pos)));
|
||||
let object = page
|
||||
.filter(|p| object_based(cx.doc, *p))
|
||||
.and_then(|p| cx.hit(ev.pos).map(|(_, id)| id).filter(|id| cx.doc.page_of_item(*id) == Some(p)));
|
||||
let ids: Vec<u64> = object.map(|id| id.0).into_iter().collect();
|
||||
if page == cx.selection.page && cx.selection.items.iter().map(|i| i.0).eq(ids.iter().copied()) {
|
||||
return vec![];
|
||||
}
|
||||
vec![Action::Exec("layout.selectPage".into(), json!({"page": page.map(|p| p + 1), "ids": ids}))]
|
||||
}
|
||||
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 => {
|
||||
self.hover = Self::control_at(cx, ev.pos).map(|(_, _, c)| (c, ev.pos));
|
||||
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((_, l, object, page)) = Self::liquid_target(cx) {
|
||||
v.push(Overlay::LiquidPins {
|
||||
object,
|
||||
page,
|
||||
pins: [l.pin_top, l.pin_right, l.pin_bottom, l.pin_left],
|
||||
resize: [l.resize_width, l.resize_height],
|
||||
});
|
||||
if let Some((c, p)) = self.hover {
|
||||
v.push(Overlay::Tip { p, text: c.tip(&l).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 {
|
||||
if self.drag.is_none() && Self::control_at(cx, p).is_some() {
|
||||
return Cursor::Hand;
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -296,14 +296,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, "ids": []}))]);
|
||||
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]
|
||||
|
||||
@@ -454,7 +454,12 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
designcraft_doc::Orientation::Horizontal => (pos2(rect.min.x, p.y), pos2(rect.max.x, p.y)),
|
||||
designcraft_doc::Orientation::Vertical => (pos2(p.x, rect.min.y), pos2(p.x, rect.max.y)),
|
||||
};
|
||||
painter.line_segment([a, b], Stroke::new(1.0, col));
|
||||
// A new guide from the rulers with the Page tool is a liquid guide: dashed.
|
||||
if g.from.is_none() && app.session.tool_id() == "page" {
|
||||
dashed(&painter, a, b, Stroke::new(1.0, col), 6.0, 3.0);
|
||||
} else {
|
||||
painter.line_segment([a, b], Stroke::new(1.0, col));
|
||||
}
|
||||
}
|
||||
draw_frames(app, &painter, &xf, &doc, &layout);
|
||||
if app.ui.hidden_characters {
|
||||
@@ -863,8 +868,18 @@ fn draw_guides(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Document
|
||||
painter.rect_stroke(rect(br), 0.0, Stroke::new(hair(painter), c32(s.bleed_color)), StrokeKind::Middle);
|
||||
}
|
||||
let pasteboard = m.inverse().transform_rect_bbox(layout.pasteboard);
|
||||
for p in &sp.pages {
|
||||
for (pi, p) in sp.pages.iter().enumerate() {
|
||||
let pr = p.bounds();
|
||||
// Page tool › Show Parent Page Overlay: the parent page's size over the page.
|
||||
if p.show_parent_overlay
|
||||
&& let SpreadRef::Doc(si) = slot.spread
|
||||
&& let Some((ppi, ppg)) = doc.parent_page_for(doc.first_page_of_spread(si) + pi)
|
||||
&& let Some(pp) = doc.parents.get(ppi).and_then(|ps| ps.pages.get(ppg))
|
||||
{
|
||||
let r = rect(DRect::new(pr.x0, pr.y0, pr.x0 + pp.width, pr.y0 + pp.height));
|
||||
painter.rect_filled(r, 0.0, Color32::from_rgba_unmultiplied(110, 110, 230, 28));
|
||||
painter.rect_stroke(r, 0.0, Stroke::new(1.0, Color32::from_rgb(110, 110, 230)), StrokeKind::Middle);
|
||||
}
|
||||
if !app.ui.guides {
|
||||
continue;
|
||||
}
|
||||
@@ -1531,6 +1546,36 @@ fn draw_tool_overlays(app: &mut DesignApp, painter: &egui::Painter, xf: &Xf) {
|
||||
let pts = vec![c + vec2(0.0, -h), c + vec2(h, 0.0), c + vec2(0.0, h), c + vec2(-h, 0.0)];
|
||||
painter.add(egui::Shape::convex_polygon(pts, tok.live_corner, Stroke::new(1.0, sel_color)));
|
||||
}
|
||||
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::Tip { p, text } => {
|
||||
let text = crate::i18n::tr(&app.ui.language, &text).to_owned();
|
||||
let s = xf.to_screen(p) + vec2(14.0, 14.0);
|
||||
let g = painter.layout(text, egui::FontId::proportional(11.0), tok.measure_text, 260.0);
|
||||
let r = Rect::from_min_size(s, g.size() + vec2(10.0, 6.0));
|
||||
painter.rect_filled(r, 3.0, tok.measure_bg);
|
||||
painter.galley(r.min + vec2(5.0, 3.0), g, tok.measure_text);
|
||||
}
|
||||
Overlay::LiquidPins { object, page, pins, resize } => draw_liquid_pins(painter, xf.rect(object), xf.rect(page), pins, resize),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1618,6 +1663,49 @@ fn ghost_texture(ctx: &egui::Context, doc: &Document, g: &designcraft_doc::Graph
|
||||
Some(t)
|
||||
}
|
||||
|
||||
/// An object's liquid controls on an object-based page, as InDesign draws them in orange: a
|
||||
/// dashed line across the object for width and height with a padlock (closed: locked), and a pin
|
||||
/// outside each edge (pinned: a line to the page edge).
|
||||
fn draw_liquid_pins(painter: &egui::Painter, o: Rect, page: Rect, pins: [bool; 4], resize: [bool; 2]) {
|
||||
let col = Color32::from_rgb(178, 88, 18);
|
||||
let st = Stroke::new(1.5, col);
|
||||
let c = o.center();
|
||||
let mids = [pos2(c.x, o.min.y), pos2(o.max.x, c.y), pos2(c.x, o.max.y), pos2(o.min.x, c.y)];
|
||||
// The width and height lines between the edges' middles.
|
||||
for (a, b) in [(mids[3], mids[1]), (mids[0], mids[2])] {
|
||||
dashed(painter, a, b, Stroke::new(1.0, col), 3.0, 3.0);
|
||||
}
|
||||
for m in mids {
|
||||
painter.circle_filled(m, 3.5, col);
|
||||
}
|
||||
let (po, lo) = (designcraft_tools::PIN_OFFSET_PX as f32, designcraft_tools::LOCK_OFFSET_PX as f32);
|
||||
let outward = [vec2(0.0, -1.0), vec2(1.0, 0.0), vec2(0.0, 1.0), vec2(-1.0, 0.0)];
|
||||
let edge = [pos2(c.x, page.min.y), pos2(page.max.x, c.y), pos2(c.x, page.max.y), pos2(page.min.x, c.y)];
|
||||
for i in 0..4 {
|
||||
if pins[i] {
|
||||
painter.line_segment([mids[i], edge[i]], st);
|
||||
painter.circle_filled(edge[i], 4.0, col);
|
||||
} else {
|
||||
painter.circle_stroke(mids[i] + outward[i] * po, 4.0, st);
|
||||
}
|
||||
}
|
||||
// Padlocks: the width lock right of the centre, the height lock above it.
|
||||
for (at, locked) in [(c + vec2(lo, 0.0), !resize[0]), (c + vec2(0.0, -lo), !resize[1])] {
|
||||
let body = Rect::from_center_size(at + vec2(0.0, 2.0), vec2(8.0, 6.0));
|
||||
painter.rect_filled(body.expand(1.0), 1.0, Color32::WHITE);
|
||||
painter.rect_filled(body, 1.0, col);
|
||||
// The shackle: over the body when locked, swung open when not.
|
||||
let base = if locked { body.center().x } else { body.center().x + 3.0 };
|
||||
let pts: Vec<_> = (0..=8)
|
||||
.map(|k| {
|
||||
let a = std::f32::consts::PI * k as f32 / 8.0;
|
||||
pos2(base - 2.6 * a.cos(), body.min.y - 3.0 * a.sin())
|
||||
})
|
||||
.collect();
|
||||
painter.add(egui::Shape::line(pts, Stroke::new(1.3, col)));
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_rulers(app: &DesignApp, ui: &egui::Ui, full: Rect, rect: Rect, xf: &Xf, doc: &Document, layout: &CanvasLayout, t: &Tokens) {
|
||||
let painter = ui.painter_at(full);
|
||||
@@ -2615,7 +2703,10 @@ fn guide_drag(app: &mut DesignApp, ui: &mut egui::Ui, resp: &egui::Response, rec
|
||||
let _ = app.run("guide.move", json!({"spread": r, "page": pi, "index": gi, "position": pos_of(sp)}));
|
||||
}
|
||||
(None, true, Some((r, sp, on_page))) => {
|
||||
let mut params = json!({"spread": r, "orientation": orient, "position": pos_of(sp), "at": sp.x, "spreadGuide": on_page.is_none()});
|
||||
// The Page tool draws liquid guides from the rulers.
|
||||
let liquid = app.session.tool_id() == "page" && on_page.is_some();
|
||||
let mut params =
|
||||
json!({"spread": r, "orientation": orient, "position": pos_of(sp), "at": sp.x, "spreadGuide": on_page.is_none(), "liquid": liquid});
|
||||
if let Some(pi) = on_page {
|
||||
params["page"] = json!(pi);
|
||||
}
|
||||
@@ -2713,6 +2804,42 @@ mod tests {
|
||||
assert!(app.canvas.shown.is_none());
|
||||
}
|
||||
|
||||
/// A primary-button drag from `a` to `b` over a few frames.
|
||||
fn drag(h: &mut Harness<'_, DesignApp>, a: Pos2, b: Pos2) {
|
||||
h.hover_at(a);
|
||||
h.step();
|
||||
h.event(egui::Event::PointerButton { pos: a, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.step();
|
||||
for k in 1..=4 {
|
||||
h.hover_at(a + (b - a) * (k as f32 / 4.0));
|
||||
h.step();
|
||||
}
|
||||
h.event(egui::Event::PointerButton { pos: b, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_page_tool_draws_liquid_guides_from_the_rulers() {
|
||||
// Chris Cicchino's Page Tool and Liquid Layout video (11:00): with the Page tool, a guide
|
||||
// dragged from the ruler is a liquid guide (dashed).
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
app.session.set_tool("page");
|
||||
let mut h = harness(app);
|
||||
let r = h.state().canvas_rect.unwrap();
|
||||
let guides = |h: &Harness<'_, DesignApp>| h.state().session.doc().unwrap().doc.spreads[0].pages[0].guides.clone();
|
||||
drag(&mut h, pos2(r.center().x, r.min.y - 8.0), r.center());
|
||||
let g = guides(&h);
|
||||
assert_eq!(g.len(), 1, "{g:?}");
|
||||
assert!(g[0].liquid && g[0].orientation == designcraft_doc::Orientation::Horizontal);
|
||||
// With the Selection tool it's a ruler guide.
|
||||
h.state_mut().session.set_tool("selection");
|
||||
drag(&mut h, pos2(r.center().x, r.min.y - 8.0), r.center() + vec2(0.0, 40.0));
|
||||
let g = guides(&h);
|
||||
assert_eq!(g.len(), 2);
|
||||
assert!(!g[1].liquid);
|
||||
}
|
||||
|
||||
fn right_click(h: &mut Harness<'_, DesignApp>, p: Pos2) {
|
||||
h.hover_at(p);
|
||||
h.step();
|
||||
|
||||
@@ -465,7 +465,9 @@ pub fn control_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
ui.horizontal_top(|ui| {
|
||||
let text_mode =
|
||||
matches!(app.session.tool_id(), "type" | "verticalType") || app.session.active().is_some_and(|d| d.selection.text.is_some());
|
||||
if text_mode {
|
||||
if app.session.tool_id() == "page" {
|
||||
control_page(app, ui);
|
||||
} else if text_mode {
|
||||
control_text(app, ui);
|
||||
} else {
|
||||
crate::object_control::show(app, ui);
|
||||
@@ -475,6 +477,93 @@ pub fn control_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The Control panel with the Page tool, as InDesign's: reference point, X/Y, W/H, page-size
|
||||
/// preset, orientation, Liquid Page Rule and Show Parent Page Overlay, for
|
||||
/// the page the Page tool selected (else the page in view). W/H keep the reference point in place.
|
||||
fn control_page(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let lang = app.ui.language.clone();
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let units = st.doc.settings.horizontal_units;
|
||||
let Some(page) = st.selection.page.or_else(|| crate::canvas::current_page(app)) else { return };
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let d = st.doc.clone();
|
||||
let Some((si, pi)) = d.page_loc(page) else { return };
|
||||
let pg = d.spreads[si].pages[pi].clone();
|
||||
let n = page + 1;
|
||||
// Reference point: W/H edits keep it in place.
|
||||
let (r, _) = ui.allocate_exact_size(vec2(30.0, 44.0), Sense::hover());
|
||||
if let Some(p) = panels::properties::ref_point_proxy(ui, egui::Rect::from_center_size(r.center(), vec2(27.0, 27.0)), app.ui.ref_point) {
|
||||
app.ui.ref_point = p;
|
||||
}
|
||||
let anchor = app.ui.ref_point.min(8);
|
||||
let (fx, fy) = ((anchor % 3) as f64 / 2.0, (anchor / 3) as f64 / 2.0);
|
||||
// The reference point's position: from the spine across, from the top down.
|
||||
let (x, y) = (pg.x - d.spreads[si].spine_x() + fx * pg.width, fy * pg.height);
|
||||
egui::Grid::new("ctl_page_xywh").num_columns(4).spacing(vec2(4.0, 4.0)).show(ui, |ui| {
|
||||
caption(ui, crate::i18n::tr(&lang, "X:"));
|
||||
// Pages don't move within their spread yet.
|
||||
ui.add_enabled_ui(false, |ui| measure(ui, "cpx", Some(x), units, 64.0));
|
||||
caption(ui, crate::i18n::tr(&lang, "W:"));
|
||||
if let Some(v) = measure(ui, "cpw", Some(pg.width), units, 64.0) {
|
||||
let _ = app.run("layout.pageSize", json!({"pages": [n], "width": v, "anchor": anchor}));
|
||||
}
|
||||
ui.end_row();
|
||||
caption(ui, crate::i18n::tr(&lang, "Y:"));
|
||||
ui.add_enabled_ui(false, |ui| measure(ui, "cpy", Some(y), units, 64.0));
|
||||
caption(ui, crate::i18n::tr(&lang, "H:"));
|
||||
if let Some(v) = measure(ui, "cph", Some(pg.height), units, 64.0) {
|
||||
let _ = app.run("layout.pageSize", json!({"pages": [n], "height": v, "anchor": anchor}));
|
||||
}
|
||||
ui.end_row();
|
||||
});
|
||||
vsep(ui);
|
||||
// Page-size preset and orientation.
|
||||
let landscape = pg.width > pg.height;
|
||||
ui.vertical(|ui| {
|
||||
let cur = panels::properties::preset_name(pg.width, pg.height);
|
||||
egui::ComboBox::from_id_salt("ctl_page_preset").selected_text(crate::rtl::widget(ui, crate::i18n::tr(&lang, cur))).width(130.0).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
for (name, w, h) in panels::properties::PAGE_PRESETS {
|
||||
if ui.selectable_label(cur == *name, crate::rtl::widget(ui, crate::i18n::tr(&lang, name))).clicked() {
|
||||
let (w, h) = if landscape { (h, w) } else { (w, h) };
|
||||
let _ = app.run("layout.pageSize", json!({"pages": [n], "width": w, "height": h, "anchor": anchor}));
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
for (icon, tip, want) in [("orient-portrait", "Portrait", false), ("orient-landscape", "Landscape", true)] {
|
||||
if crate::widgets::icon_toggle(ui, icon, landscape == want, crate::i18n::tr(&lang, tip)).clicked() && landscape != want {
|
||||
let _ = app.run("layout.pageSize", json!({"pages": [n], "width": pg.height, "height": pg.width, "anchor": anchor}));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
vsep(ui);
|
||||
ui.vertical(|ui| {
|
||||
caption(ui, crate::i18n::tr(&lang, "Liquid Page Rule:"));
|
||||
let label = panels::interactive::liquid_rule_label(pg.liquid);
|
||||
egui::ComboBox::from_id_salt("ctl_page_rule").selected_text(crate::rtl::widget(ui, crate::i18n::tr(&lang, label))).width(150.0).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
for (r, l) in panels::interactive::LIQUID_RULES {
|
||||
if ui.selectable_label(pg.liquid == r, crate::rtl::widget(ui, crate::i18n::tr(&lang, l))).clicked() && pg.liquid != r {
|
||||
let _ = app.run("liquid.pageRule", json!({"rule": r, "pages": [n]}));
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
vsep(ui);
|
||||
ui.vertical(|ui| {
|
||||
let mut overlay = pg.show_parent_overlay;
|
||||
if ui.checkbox(&mut overlay, crate::rtl::widget(ui, crate::i18n::tr(&lang, "Show Parent Page Overlay"))).changed() {
|
||||
let _ = app.run("layout.pageOptions", json!({"pages": [n], "showParentOverlay": overlay}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn control_text(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let attrs = panels::text_attrs(app);
|
||||
@@ -1153,6 +1242,25 @@ mod tests {
|
||||
assert!(short.min.x - first.min.x <= 2.0 * super::DOC_TAB_MAX_W, "the last tab {short:?} starts too far right");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_page_tool_has_its_own_control_panel() {
|
||||
// Chris Cicchino's Page Tool and Liquid Layout video (1:02): with the Page tool, the
|
||||
// Control panel shows the page's size, preset, orientation and liquid page rule.
|
||||
let mut app = app();
|
||||
app.run("file.new", serde_json::json!({"width": 252, "height": 144, "facingPages": false})).unwrap();
|
||||
app.session.set_tool("page");
|
||||
let mut h = test_window::open(app, vec2(1800.0, 600.0));
|
||||
let _ = h.get_by_label("Liquid Page Rule:");
|
||||
// Pages don't move within their spread, so there is no "Objects Move with Page" option, and
|
||||
// no "Controlled by Parent" rule until parent pages have rules of their own.
|
||||
assert!(h.query_by_label("Objects Move with Page").is_none());
|
||||
assert!(!crate::panels::interactive::LIQUID_RULES.iter().any(|(r, _)| *r == designcraft_doc::LiquidRule::ControlledByParent));
|
||||
assert_eq!(crate::panels::properties::preset_name(252.0, 144.0), "US Business Card");
|
||||
h.get_by_label("Show Parent Page Overlay").click();
|
||||
h.run_steps(4);
|
||||
assert!(h.state().app.session.doc().unwrap().doc.spreads[0].pages[0].show_parent_overlay);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_narrow_window_scrolls_the_control_bar() {
|
||||
let w = 700.0;
|
||||
|
||||
@@ -3276,6 +3276,96 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Added", ["Added", "Ajouté", "Added", "Added", "已添加"]),
|
||||
("Deleted", ["Deleted", "Supprimé", "Deleted", "Deleted", "已删除"]),
|
||||
("Guide-based", ["Guide-based", "Basé sur les repères", "Guide-based", "Guide-based", "基于参考线"]),
|
||||
("Object-Based", ["Object-Based", "Basé sur les objets", "Object-Based", "Object-Based", "基于对象"]),
|
||||
("Guide-Based", ["Guide-Based", "Basé sur les repères", "Guide-Based", "Guide-Based", "基于参考线"]),
|
||||
("Page:", ["Seite:", "Page :", "Página:", "ページ:", "页面:"]),
|
||||
("Content:", ["Inhalt:", "Contenu :", "Contenido:", "コンテンツ:", "内容:"]),
|
||||
("Object Constraints:", ["Objektbeschränkungen:", "Contraintes d’objet :", "Restricciones de objeto:", "オブジェクトの制約:", "对象约束:"]),
|
||||
(
|
||||
"Resize with Page:",
|
||||
["Mit Seite skalieren:", "Redimensionner avec la page :", "Cambiar tamaño con la página:", "ページと共にサイズ変更:", "随页面调整大小:"],
|
||||
),
|
||||
("Pin:", ["Fixieren:", "Épingler :", "Fijar:", "固定:", "固定:"]),
|
||||
("Resize Page", ["Seitengröße ändern", "Redimensionner la page", "Cambiar tamaño de página", "ページのサイズを変更", "调整页面大小"]),
|
||||
(
|
||||
"Liquid Page Rule:",
|
||||
["Regel für flüssige Seiten:", "Règle de page liquide :", "Regla de página líquida:", "リキッドページルール:", "自适应页面规则:"],
|
||||
),
|
||||
(
|
||||
"Show Parent Page Overlay",
|
||||
[
|
||||
"Überlagerung der übergeordneten Seite anzeigen",
|
||||
"Afficher la superposition de la page parent",
|
||||
"Mostrar superposición de página principal",
|
||||
"親ページオーバーレイを表示",
|
||||
"显示父页面叠加",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Controlled by Parent",
|
||||
["Durch übergeordnete Seite gesteuert", "Contrôlé par le parent", "Controlado por principal", "親ページで制御", "由父页面控制"],
|
||||
),
|
||||
("US Business Card", ["US-Visitenkarte", "Carte de visite US", "Tarjeta de visita EE. UU.", "US 名刺", "美国名片"]),
|
||||
(
|
||||
"Object is pinned relative to the page edge. Click to release.",
|
||||
[
|
||||
"Das Objekt ist relativ zum Seitenrand fixiert. Zum Lösen klicken.",
|
||||
"L’objet est épinglé par rapport au bord de la page. Cliquez pour le libérer.",
|
||||
"El objeto está fijado respecto al borde de la página. Haga clic para soltarlo.",
|
||||
"オブジェクトはページの端を基準に固定されています。クリックすると解除します。",
|
||||
"对象已相对于页面边缘固定。单击可释放。",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Click to pin the object relative to the page edge.",
|
||||
[
|
||||
"Klicken, um das Objekt relativ zum Seitenrand zu fixieren.",
|
||||
"Cliquez pour épingler l’objet par rapport au bord de la page.",
|
||||
"Haga clic para fijar el objeto respecto al borde de la página.",
|
||||
"クリックすると、オブジェクトをページの端を基準に固定します。",
|
||||
"单击可将对象相对于页面边缘固定。",
|
||||
],
|
||||
),
|
||||
(
|
||||
"The object changes width as the page changes width. Click to lock the object width.",
|
||||
[
|
||||
"Die Objektbreite ändert sich mit der Seitenbreite. Zum Sperren der Breite klicken.",
|
||||
"La largeur de l’objet suit celle de la page. Cliquez pour verrouiller la largeur.",
|
||||
"El ancho del objeto cambia con el de la página. Haga clic para bloquear el ancho.",
|
||||
"オブジェクトの幅はページの幅に合わせて変わります。クリックすると幅をロックします。",
|
||||
"对象宽度随页面宽度变化。单击可锁定对象宽度。",
|
||||
],
|
||||
),
|
||||
(
|
||||
"The object's width is locked. Click to let the object change width as the page changes width.",
|
||||
[
|
||||
"Die Objektbreite ist gesperrt. Klicken, damit sie sich mit der Seitenbreite ändert.",
|
||||
"La largeur de l’objet est verrouillée. Cliquez pour qu’elle suive celle de la page.",
|
||||
"El ancho del objeto está bloqueado. Haga clic para que cambie con el de la página.",
|
||||
"オブジェクトの幅はロックされています。クリックすると、ページの幅に合わせて変わります。",
|
||||
"对象宽度已锁定。单击可让其随页面宽度变化。",
|
||||
],
|
||||
),
|
||||
(
|
||||
"The object changes height as the page changes height. Click to lock the object height.",
|
||||
[
|
||||
"Die Objekthöhe ändert sich mit der Seitenhöhe. Zum Sperren der Höhe klicken.",
|
||||
"La hauteur de l’objet suit celle de la page. Cliquez pour verrouiller la hauteur.",
|
||||
"El alto del objeto cambia con el de la página. Haga clic para bloquear el alto.",
|
||||
"オブジェクトの高さはページの高さに合わせて変わります。クリックすると高さをロックします。",
|
||||
"对象高度随页面高度变化。单击可锁定对象高度。",
|
||||
],
|
||||
),
|
||||
(
|
||||
"The object's height is locked. Click to let the object change height as the page changes height.",
|
||||
[
|
||||
"Die Objekthöhe ist gesperrt. Klicken, damit sie sich mit der Seitenhöhe ändert.",
|
||||
"La hauteur de l’objet est verrouillée. Cliquez pour qu’elle suive celle de la page.",
|
||||
"El alto del objeto está bloqueado. Haga clic para que cambie con el de la página.",
|
||||
"オブジェクトの高さはロックされています。クリックすると、ページの高さに合わせて変わります。",
|
||||
"对象高度已锁定。单击可让其随页面高度变化。",
|
||||
],
|
||||
),
|
||||
("Loop", ["Loop", "Boucle", "Loop", "Loop", "循环播放"]),
|
||||
("Object-based", ["Object-based", "Basé sur les objets", "Object-based", "Object-based", "基于对象"]),
|
||||
("Off", ["Off", "Désactivé", "Off", "Off", "关闭"]),
|
||||
|
||||
@@ -766,6 +766,36 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "مضاف"),
|
||||
("Deleted", "محذوف"),
|
||||
("Guide-based", "استنادًا إلى الأدلة"),
|
||||
("Page:", "الصفحة:"),
|
||||
("Content:", "المحتوى:"),
|
||||
("Object Constraints:", "قيود الكائن:"),
|
||||
("Resize with Page:", "تغيير الحجم مع الصفحة:"),
|
||||
("Pin:", "تثبيت:"),
|
||||
("Object-Based", "استنادًا إلى الكائنات"),
|
||||
("Guide-Based", "استنادًا إلى الأدلة"),
|
||||
("Resize Page", "تغيير حجم الصفحة"),
|
||||
("Liquid Page Rule:", "قاعدة الصفحة المرنة:"),
|
||||
("Show Parent Page Overlay", "إظهار تراكب الصفحة الأصل"),
|
||||
("Controlled by Parent", "تتحكم فيه الصفحة الأصل"),
|
||||
("US Business Card", "بطاقة عمل أمريكية"),
|
||||
("Object is pinned relative to the page edge. Click to release.", "الكائن مثبت بالنسبة إلى حافة الصفحة. انقر لتحريره."),
|
||||
("Click to pin the object relative to the page edge.", "انقر لتثبيت الكائن بالنسبة إلى حافة الصفحة."),
|
||||
(
|
||||
"The object changes width as the page changes width. Click to lock the object width.",
|
||||
"يتغير عرض الكائن مع تغير عرض الصفحة. انقر لقفل عرض الكائن.",
|
||||
),
|
||||
(
|
||||
"The object's width is locked. Click to let the object change width as the page changes width.",
|
||||
"عرض الكائن مقفل. انقر للسماح بتغيره مع عرض الصفحة.",
|
||||
),
|
||||
(
|
||||
"The object changes height as the page changes height. Click to lock the object height.",
|
||||
"يتغير ارتفاع الكائن مع تغير ارتفاع الصفحة. انقر لقفل ارتفاع الكائن.",
|
||||
),
|
||||
(
|
||||
"The object's height is locked. Click to let the object change height as the page changes height.",
|
||||
"ارتفاع الكائن مقفل. انقر للسماح بتغيره مع ارتفاع الصفحة.",
|
||||
),
|
||||
("Loop", "تكرار"),
|
||||
("Object-based", "استنادًا إلى الكائنات"),
|
||||
("Play on Page Load", "تشغيل عند تحميل الصفحة"),
|
||||
|
||||
@@ -1354,6 +1354,36 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "Aggiunto"),
|
||||
("Deleted", "Eliminato"),
|
||||
("Guide-based", "Basato su guide"),
|
||||
("Page:", "Pagina:"),
|
||||
("Content:", "Contenuto:"),
|
||||
("Object Constraints:", "Vincoli oggetto:"),
|
||||
("Resize with Page:", "Ridimensiona con la pagina:"),
|
||||
("Pin:", "Fissa:"),
|
||||
("Object-Based", "Basato su oggetti"),
|
||||
("Guide-Based", "Basato su guide"),
|
||||
("Resize Page", "Ridimensiona pagina"),
|
||||
("Liquid Page Rule:", "Regola pagina liquida:"),
|
||||
("Show Parent Page Overlay", "Mostra sovrapposizione pagina principale"),
|
||||
("Controlled by Parent", "Controllato dalla pagina principale"),
|
||||
("US Business Card", "Biglietto da visita USA"),
|
||||
("Object is pinned relative to the page edge. Click to release.", "L’oggetto è fissato rispetto al bordo della pagina. Fai clic per sbloccarlo."),
|
||||
("Click to pin the object relative to the page edge.", "Fai clic per fissare l’oggetto rispetto al bordo della pagina."),
|
||||
(
|
||||
"The object changes width as the page changes width. Click to lock the object width.",
|
||||
"La larghezza dell’oggetto cambia con quella della pagina. Fai clic per bloccarla.",
|
||||
),
|
||||
(
|
||||
"The object's width is locked. Click to let the object change width as the page changes width.",
|
||||
"La larghezza dell’oggetto è bloccata. Fai clic perché cambi con quella della pagina.",
|
||||
),
|
||||
(
|
||||
"The object changes height as the page changes height. Click to lock the object height.",
|
||||
"L’altezza dell’oggetto cambia con quella della pagina. Fai clic per bloccarla.",
|
||||
),
|
||||
(
|
||||
"The object's height is locked. Click to let the object change height as the page changes height.",
|
||||
"L’altezza dell’oggetto è bloccata. Fai clic perché cambi con quella della pagina.",
|
||||
),
|
||||
("Loop", "Ciclo continuo"),
|
||||
("Object-based", "Basato su oggetti"),
|
||||
("Off", "Disattivato"),
|
||||
|
||||
@@ -1474,6 +1474,36 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "追加済み"),
|
||||
("Deleted", "削除済み"),
|
||||
("Guide-based", "ガイド基準"),
|
||||
("Page:", "ページ:"),
|
||||
("Content:", "コンテンツ:"),
|
||||
("Object Constraints:", "オブジェクトの制約:"),
|
||||
("Resize with Page:", "ページと共にサイズ変更:"),
|
||||
("Pin:", "固定:"),
|
||||
("Object-Based", "オブジェクト基準"),
|
||||
("Guide-Based", "ガイド基準"),
|
||||
("Resize Page", "ページのサイズを変更"),
|
||||
("Liquid Page Rule:", "リキッドページルール:"),
|
||||
("Show Parent Page Overlay", "親ページオーバーレイを表示"),
|
||||
("Controlled by Parent", "親ページで制御"),
|
||||
("US Business Card", "US 名刺"),
|
||||
("Object is pinned relative to the page edge. Click to release.", "オブジェクトはページの端を基準に固定されています。クリックすると解除します。"),
|
||||
("Click to pin the object relative to the page edge.", "クリックすると、オブジェクトをページの端を基準に固定します。"),
|
||||
(
|
||||
"The object changes width as the page changes width. Click to lock the object width.",
|
||||
"オブジェクトの幅はページの幅に合わせて変わります。クリックすると幅をロックします。",
|
||||
),
|
||||
(
|
||||
"The object's width is locked. Click to let the object change width as the page changes width.",
|
||||
"オブジェクトの幅はロックされています。クリックすると、ページの幅に合わせて変わります。",
|
||||
),
|
||||
(
|
||||
"The object changes height as the page changes height. Click to lock the object height.",
|
||||
"オブジェクトの高さはページの高さに合わせて変わります。クリックすると高さをロックします。",
|
||||
),
|
||||
(
|
||||
"The object's height is locked. Click to let the object change height as the page changes height.",
|
||||
"オブジェクトの高さはロックされています。クリックすると、ページの高さに合わせて変わります。",
|
||||
),
|
||||
("Loop", "繰り返し再生"),
|
||||
("Object-based", "オブジェクト基準"),
|
||||
("Off", "オフ"),
|
||||
|
||||
@@ -758,6 +758,36 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "Adicionado"),
|
||||
("Deleted", "Excluído"),
|
||||
("Guide-based", "Baseado em guias"),
|
||||
("Page:", "Página:"),
|
||||
("Content:", "Conteúdo:"),
|
||||
("Object Constraints:", "Restrições de objeto:"),
|
||||
("Resize with Page:", "Redimensionar com a página:"),
|
||||
("Pin:", "Fixar:"),
|
||||
("Object-Based", "Baseado em objetos"),
|
||||
("Guide-Based", "Baseado em guias"),
|
||||
("Resize Page", "Redimensionar página"),
|
||||
("Liquid Page Rule:", "Regra de página líquida:"),
|
||||
("Show Parent Page Overlay", "Mostrar sobreposição da página principal"),
|
||||
("Controlled by Parent", "Controlado pela página principal"),
|
||||
("US Business Card", "Cartão de visita EUA"),
|
||||
("Object is pinned relative to the page edge. Click to release.", "O objeto está fixado em relação à borda da página. Clique para liberar."),
|
||||
("Click to pin the object relative to the page edge.", "Clique para fixar o objeto em relação à borda da página."),
|
||||
(
|
||||
"The object changes width as the page changes width. Click to lock the object width.",
|
||||
"A largura do objeto muda com a da página. Clique para bloquear a largura.",
|
||||
),
|
||||
(
|
||||
"The object's width is locked. Click to let the object change width as the page changes width.",
|
||||
"A largura do objeto está bloqueada. Clique para que mude com a da página.",
|
||||
),
|
||||
(
|
||||
"The object changes height as the page changes height. Click to lock the object height.",
|
||||
"A altura do objeto muda com a da página. Clique para bloquear a altura.",
|
||||
),
|
||||
(
|
||||
"The object's height is locked. Click to let the object change height as the page changes height.",
|
||||
"A altura do objeto está bloqueada. Clique para que mude com a da página.",
|
||||
),
|
||||
("Loop", "Repetir"),
|
||||
("Object-based", "Baseado em objetos"),
|
||||
("Play on Page Load", "Reproduzir ao carregar a página"),
|
||||
|
||||
@@ -1349,6 +1349,36 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "Додано"),
|
||||
("Deleted", "Видалено"),
|
||||
("Guide-based", "На основі напрямних"),
|
||||
("Page:", "Сторінка:"),
|
||||
("Content:", "Вміст:"),
|
||||
("Object Constraints:", "Обмеження об’єкта:"),
|
||||
("Resize with Page:", "Змінювати розмір зі сторінкою:"),
|
||||
("Pin:", "Закріпити:"),
|
||||
("Object-Based", "На основі об’єктів"),
|
||||
("Guide-Based", "На основі напрямних"),
|
||||
("Resize Page", "Змінити розмір сторінки"),
|
||||
("Liquid Page Rule:", "Правило гнучкої сторінки:"),
|
||||
("Show Parent Page Overlay", "Показати накладення батьківської сторінки"),
|
||||
("Controlled by Parent", "Керується батьківською сторінкою"),
|
||||
("US Business Card", "Візитна картка (США)"),
|
||||
("Object is pinned relative to the page edge. Click to release.", "Об’єкт закріплено відносно краю сторінки. Клацніть, щоб звільнити."),
|
||||
("Click to pin the object relative to the page edge.", "Клацніть, щоб закріпити об’єкт відносно краю сторінки."),
|
||||
(
|
||||
"The object changes width as the page changes width. Click to lock the object width.",
|
||||
"Ширина об’єкта змінюється разом зі сторінкою. Клацніть, щоб заблокувати ширину.",
|
||||
),
|
||||
(
|
||||
"The object's width is locked. Click to let the object change width as the page changes width.",
|
||||
"Ширину об’єкта заблоковано. Клацніть, щоб вона змінювалася разом зі сторінкою.",
|
||||
),
|
||||
(
|
||||
"The object changes height as the page changes height. Click to lock the object height.",
|
||||
"Висота об’єкта змінюється разом зі сторінкою. Клацніть, щоб заблокувати висоту.",
|
||||
),
|
||||
(
|
||||
"The object's height is locked. Click to let the object change height as the page changes height.",
|
||||
"Висоту об’єкта заблоковано. Клацніть, щоб вона змінювалася разом зі сторінкою.",
|
||||
),
|
||||
("Loop", "Зациклити"),
|
||||
("Object-based", "На основі об’єктів"),
|
||||
("Off", "Вимкнено"),
|
||||
|
||||
@@ -382,6 +382,7 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
||||
"<",
|
||||
"cmd:layout.marginsAndColumns",
|
||||
"cmd:layout.createGuides",
|
||||
"cmd:layout.createAlternate",
|
||||
"-",
|
||||
"ui:view.goToPage",
|
||||
"-",
|
||||
|
||||
@@ -489,64 +489,118 @@ pub fn buttons(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Liquid Layout panel: the current page's rule, the selection's pins (object-based), the page's
|
||||
/// liquid guides, and Create Alternate Layout.
|
||||
/// Liquid page rules in InDesign's order, with their menu labels.
|
||||
pub const LIQUID_RULES: [(designcraft_doc::LiquidRule, &str); 5] = [
|
||||
(designcraft_doc::LiquidRule::Off, "Off"),
|
||||
(designcraft_doc::LiquidRule::Scale, "Scale"),
|
||||
(designcraft_doc::LiquidRule::ReCenter, "Re-center"),
|
||||
(designcraft_doc::LiquidRule::ObjectBased, "Object-based"),
|
||||
(designcraft_doc::LiquidRule::GuideBased, "Guide-based"),
|
||||
];
|
||||
|
||||
/// A liquid page rule's menu label.
|
||||
pub fn liquid_rule_label(rule: designcraft_doc::LiquidRule) -> &'static str {
|
||||
LIQUID_RULES.iter().find(|(r, _)| *r == rule).map_or("Off", |(_, l)| l)
|
||||
}
|
||||
|
||||
/// Whether page `abs` follows the object-based rule (its own, or its parent page's).
|
||||
fn object_based(d: &designcraft_doc::Document, abs: usize) -> bool {
|
||||
use designcraft_doc::LiquidRule as L;
|
||||
let Some((si, pi)) = d.page_loc(abs) else { return false };
|
||||
match d.spreads[si].pages[pi].liquid {
|
||||
L::ObjectBased => true,
|
||||
L::ControlledByParent => {
|
||||
d.parent_page_for(abs).and_then(|(ppi, ppg)| d.parents.get(ppi)?.pages.get(ppg).map(|p| p.liquid == L::ObjectBased)).unwrap_or(false)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Liquid Layout panel, laid out as InDesign's: the page's Liquid Page Rule (with "Page: n"),
|
||||
/// Content › Auto-Fit, and Object Constraints › Resize with Page (Height, Width) and Pin (Top,
|
||||
/// Left, Bottom, Right) for the selected object on an object-based page. The page's liquid
|
||||
/// guides follow (Create Alternate Layout is in the Layout menu).
|
||||
pub fn liquid(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some(page) = crate::canvas::current_page(app) else {
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "No document.")).size(11.0).color(t.text_dim));
|
||||
let lang = app.ui.language.clone();
|
||||
// The Page tool's page, the page in view, or (before the canvas lays out) the first.
|
||||
let Some(page) = app.session.active().map(|s| s.selection.page.or_else(|| crate::canvas::current_page(app)).unwrap_or(0)) else {
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&lang, "No document.")).size(11.0).color(t.text_dim));
|
||||
return;
|
||||
};
|
||||
let Some(d) = app.session.active().map(|d| d.doc.clone()) else { return };
|
||||
let Some((si, pi)) = d.page_loc(page) else { return };
|
||||
let pg = d.spreads[si].pages[pi].clone();
|
||||
use designcraft_doc::LiquidRule as L;
|
||||
crate::rtl::label(ui, egui::RichText::new(format!("{} {}", crate::i18n::tr(&app.ui.language, "Page"), d.page_name(page))).strong());
|
||||
let mut rule = pg.liquid;
|
||||
egui::ComboBox::from_id_salt("liquid_rule")
|
||||
.selected_text(crate::rtl::widget(
|
||||
ui,
|
||||
crate::i18n::tr(
|
||||
&app.ui.language,
|
||||
match rule {
|
||||
L::Off => "Off",
|
||||
L::Scale => "Scale",
|
||||
L::ReCenter => "Re-center",
|
||||
L::GuideBased => "Guide-based",
|
||||
L::ObjectBased => "Object-based",
|
||||
},
|
||||
),
|
||||
))
|
||||
.show_ui(ui, |ui| {
|
||||
for (r, label) in
|
||||
[(L::Off, "Off"), (L::Scale, "Scale"), (L::ReCenter, "Re-center"), (L::GuideBased, "Guide-based"), (L::ObjectBased, "Object-based")]
|
||||
{
|
||||
ui.selectable_value(&mut rule, r, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label)));
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
crate::widgets::caption(ui, crate::i18n::tr(&lang, "Liquid Page Rule:"));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
crate::widgets::caption(ui, &format!("{} {}", crate::i18n::tr(&lang, "Page:"), d.page_name(page)));
|
||||
});
|
||||
if rule != pg.liquid {
|
||||
let _ = app.run("liquid.pageRule", json!({"rule": rule, "pages": [page + 1]}));
|
||||
}
|
||||
if rule == L::ObjectBased
|
||||
&& let Some(id) = app.session.active().and_then(|s| s.selection.items.first().copied())
|
||||
&& let Some(it) = d.item(id)
|
||||
{
|
||||
ui.separator();
|
||||
let l = it.liquid.unwrap_or(designcraft_doc::ObjectLiquid { resize_width: true, resize_height: true, ..Default::default() });
|
||||
let mut v = [l.resize_width, l.resize_height, l.pin_top, l.pin_bottom, l.pin_left, l.pin_right];
|
||||
let keys = ["resizeWidth", "resizeHeight", "pinTop", "pinBottom", "pinLeft", "pinRight"];
|
||||
let labels = ["Resize width", "Resize height", "Pin top", "Pin bottom", "Pin left", "Pin right"];
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for ((on, key), label) in v.iter_mut().zip(keys).zip(labels) {
|
||||
if ui.checkbox(on, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).changed() {
|
||||
let _ = app.run("liquid.object", json!({key: *on}));
|
||||
});
|
||||
egui::ComboBox::from_id_salt("liquid_rule")
|
||||
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&lang, liquid_rule_label(pg.liquid))))
|
||||
.width(190.0)
|
||||
.show_ui(ui, |ui| {
|
||||
for (r, label) in LIQUID_RULES {
|
||||
if ui.selectable_label(pg.liquid == r, crate::rtl::widget(ui, crate::i18n::tr(&lang, label))).clicked() && r != pg.liquid {
|
||||
let _ = app.run("liquid.pageRule", json!({"rule": r, "pages": [page + 1]}));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.separator();
|
||||
// The selected object (one on this page).
|
||||
let item = app
|
||||
.session
|
||||
.active()
|
||||
.and_then(|s| s.selection.items.first().copied())
|
||||
.and_then(|id| d.item(id))
|
||||
.filter(|it| d.page_of_item(it.id) == Some(page));
|
||||
// Content: Auto-Fit (graphic frames).
|
||||
crate::widgets::caption(ui, crate::i18n::tr(&lang, "Content:"));
|
||||
let graphic = item.and_then(|it| if let designcraft_doc::Content::Graphic(g) = &it.content { Some(g.auto_fit) } else { None });
|
||||
let mut auto = graphic.is_some_and(|f| f != designcraft_doc::Fitting::None);
|
||||
ui.indent("liquid_content", |ui| {
|
||||
let r = ui.add_enabled(graphic.is_some(), egui::Checkbox::new(&mut auto, crate::rtl::widget(ui, crate::i18n::tr(&lang, "Auto-Fit"))));
|
||||
if r.changed() {
|
||||
let _ = app.run("object.fittingOptions", json!({"autoFit": auto}));
|
||||
}
|
||||
});
|
||||
// Object Constraints: for an object on an object-based page.
|
||||
crate::widgets::caption(ui, crate::i18n::tr(&lang, "Object Constraints:"));
|
||||
let on = item.is_some() && object_based(&d, page);
|
||||
let l = item
|
||||
.filter(|_| on)
|
||||
.map(|it| it.liquid.unwrap_or(designcraft_doc::ObjectLiquid { resize_width: true, resize_height: true, ..Default::default() }))
|
||||
.unwrap_or_default();
|
||||
ui.indent("liquid_constraints", |ui| {
|
||||
ui.add_enabled_ui(on, |ui| {
|
||||
let rows: [(&str, [(&str, &str, bool); 2]); 2] = [
|
||||
("Resize with Page:", [("Height", "resizeHeight", l.resize_height), ("Width", "resizeWidth", l.resize_width)]),
|
||||
("Pin:", [("Top", "pinTop", l.pin_top), ("Left", "pinLeft", l.pin_left)]),
|
||||
];
|
||||
for (title, boxes) in rows {
|
||||
crate::widgets::caption(ui, crate::i18n::tr(&lang, title));
|
||||
let mut grid_boxes = vec![boxes.to_vec()];
|
||||
if title == "Pin:" {
|
||||
grid_boxes.push(vec![("Bottom", "pinBottom", l.pin_bottom), ("Right", "pinRight", l.pin_right)]);
|
||||
}
|
||||
egui::Grid::new(("liquid_grid", title)).num_columns(2).min_col_width(90.0).show(ui, |ui| {
|
||||
for row in grid_boxes {
|
||||
for (label, key, value) in row {
|
||||
let mut v = value;
|
||||
if ui.checkbox(&mut v, crate::rtl::widget(ui, crate::i18n::tr(&lang, label))).changed() {
|
||||
let _ = app.run("liquid.object", json!({key: v}));
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
if !pg.guides.is_empty() {
|
||||
ui.separator();
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Liquid guides")).size(11.0).color(t.text_dim));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&lang, "Liquid guides")).size(11.0).color(t.text_dim));
|
||||
for (gi, g) in pg.guides.iter().enumerate() {
|
||||
let mut on = g.liquid;
|
||||
let label = format!("{:?} at {:.1}", g.orientation, g.position);
|
||||
@@ -555,22 +609,6 @@ pub fn liquid(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
let key = egui::Id::new("alt_layout");
|
||||
let (mut name, mut w, mut h): (String, f64, f64) = ui.data(|x| x.get_temp(key)).unwrap_or(("Tablet H".into(), 1024.0, 768.0));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Create Alternate Layout")).size(11.0).color(t.text_dim));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut name).desired_width(90.0));
|
||||
ui.add(egui::DragValue::new(&mut w).range(1.0..=15552.0).suffix(" pt"));
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "×"));
|
||||
ui.add(egui::DragValue::new(&mut h).range(1.0..=15552.0).suffix(" pt"));
|
||||
});
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Create"))).clicked()
|
||||
&& let Err(e) = app.run("layout.createAlternate", json!({"name": name, "width": w, "height": h}))
|
||||
{
|
||||
app.status_error("Alternate layout", e);
|
||||
}
|
||||
ui.data_mut(|x| x.insert_temp(key, (name, w, h)));
|
||||
}
|
||||
|
||||
/// Media panel: the selected video or sound's playback options and poster.
|
||||
@@ -736,3 +774,45 @@ pub fn track_changes(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod liquid_panel_tests {
|
||||
use egui_kittest::{Harness, kittest::Queryable};
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn object_constraints_set_the_selected_objects_pins_and_resizing() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let id = app.run("frame.create", json!({"rect": [100, 100, 200, 200]})).unwrap()["id"].as_u64().unwrap();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(260.0, 420.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;
|
||||
}
|
||||
liquid(app, ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
let l = |h: &Harness<'_, DesignApp>| h.state().session.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().liquid;
|
||||
// With the rule off the constraints don't apply.
|
||||
h.get_by_label("Right").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(l(&h), None);
|
||||
h.state_mut().run("liquid.pageRule", json!({"rule": "objectBased"})).unwrap();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Right").click();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Width").click();
|
||||
h.run_steps(2);
|
||||
let v = l(&h).unwrap();
|
||||
assert!(v.pin_right && !v.resize_width && v.resize_height, "{v:?}");
|
||||
let _ = h.get_by_label("Auto-Fit");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
// ---------------------------------------------------------------- No Selection
|
||||
|
||||
/// Page-size presets for the Document dropdown (portrait, points).
|
||||
const PAGE_PRESETS: &[(&str, f64, f64)] = &[
|
||||
pub(crate) const PAGE_PRESETS: &[(&str, f64, f64)] = &[
|
||||
("Letter", 612.0, 792.0),
|
||||
("Legal", 612.0, 1008.0),
|
||||
("Tabloid", 792.0, 1224.0),
|
||||
@@ -179,9 +179,10 @@ const PAGE_PRESETS: &[(&str, f64, f64)] = &[
|
||||
("A5", 419.528, 595.276),
|
||||
("A6", 297.638, 419.528),
|
||||
("B5", 498.898, 708.661),
|
||||
("US Business Card", 144.0, 252.0),
|
||||
];
|
||||
|
||||
fn preset_name(w: f64, h: f64) -> &'static str {
|
||||
pub(crate) fn preset_name(w: f64, h: f64) -> &'static str {
|
||||
let (a, b) = (w.min(h), w.max(h));
|
||||
PAGE_PRESETS.iter().find(|p| (p.1 - a).abs() < 0.6 && (p.2 - b).abs() < 0.6).map(|p| p.0).unwrap_or("Custom")
|
||||
}
|
||||
|
||||
@@ -405,7 +405,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. With the Page tool the Control panel shows the page (reference point, W/H, preset, orientation, Liquid Page Rule, Show Parent Page Overlay); X/Y are shown, but pages don't move within their spread yet. On an object-based page it selects objects and shows their pins and width/height locks on the canvas (click to toggle, with tooltips).
|
||||
- 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