M1: ruler guides (drag out/move/delete, Create Guides); fix task-bar deadlock while dragging
- engine: guide.add/move/delete/deleteAll/list, layout.createGuides (rows/columns with gutters, margins or page) (+ test) - UI: drag a guide out of either ruler (page guide, or spread guide on the pasteboard), drag one with the selection tools to move it, drop it on a ruler to delete it; spread guides cross the pasteboard; Layout › Create Guides…, View › Grids & Guides › Delete All Guides on Spread - fix: the Contextual Task Bar called is_pointer_over_egui inside ctx.input — a deadlock that froze the app while dragging with a selection - ui_shot injects synthetic input (ui.click/ui.drag/ui.key now work offscreen) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
252ae0df32
commit
9b8983ada2
|
|
@ -22,6 +22,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
|||
- Index: page references (topic from the selection or up to 4 levels, sort keys; current page, to end of story, next n paragraphs, suppressed, See / See also), Generate / Update Index (section headings, nested or run-in, page ranges merged, Index Title / Section Head / Level 1–4 styles); IDML export/import as InDesign topics, page references and topic cross-references (verified in InDesign 2026); `index.*` commands.
|
||||
- Text clipboard keeps formatting (character/paragraph formats, footnotes, cross-references, index markers, tables) and falls back to plain text when the system clipboard changed; Paste without Formatting (⇧⌘V); Change Case (UPPERCASE, lowercase, Title Case, Sentence case — formatting kept); Type › Insert Special Character / White Space / Break Character submenus.
|
||||
- Anchored objects: items flow in the text inline (on the baseline, with Y offset; lines grow to fit) or above the line (left/center/right, space before/after); paste copied items into text to anchor them; Anchored Object Options and Release (back onto the page where shown); rendered, exported to PDF and to/from IDML (InDesign anchored object settings; verified in InDesign 2026); copy/paste and threading carry them; `anchored.*` commands.
|
||||
- Ruler guides: drag out of the rulers (page guides, or spread guides on the pasteboard), drag to move, drop on a ruler to delete; Layout › Create Guides (rows/columns with gutters, fit to margins or page); Delete All Guides on Spread; `guide.*` commands.
|
||||
- Menus expose the engine's features (TOC, numbering & sections, text variables, hyperlinks, cross-references, footnotes, step and repeat, spelling, fitting/content, effects, Data Merge, Preflight, EPUB); any menu command with parameters gets a dialog generated from its parameter documentation.
|
||||
- Table of contents (generate/update, dot leaders) and text variables (running headers, last page number, chapter number, file name, dates, custom) resolved per page.
|
||||
- Soft effects: blurred drop shadow, inner shadow, outer glow, basic feather.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,217 @@
|
|||
//! Ruler guides: add (dragged from the rulers), move, delete, list; Layout › Create Guides.
|
||||
//!
|
||||
//! Guides are stored on their page in spread coordinates; a spread guide (dropped on the
|
||||
//! pasteboard) lives on the spread's first page with `spread: true` and crosses the pasteboard.
|
||||
|
||||
use designcraft_doc::{Document, Guide, Orientation, SpreadRef};
|
||||
use designcraft_geom::Rect;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, bad, cmd, has_doc, spread_param, str_param};
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
"guide.add",
|
||||
"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} → {page, index}",
|
||||
has_doc,
|
||||
add
|
||||
),
|
||||
cmd!("guide.move", "Move Guide", [], None, "{spread?, page, index, position}", has_doc, move_guide),
|
||||
cmd!("guide.delete", "Delete Guide", [], None, "{spread?, page, index}", has_doc, delete),
|
||||
cmd!("guide.deleteAll", "Delete All Guides on Spread", ["View", "Grids & Guides"], None, "{spread?}", has_doc, |s, p| {
|
||||
let r = spread_param(p, "spread");
|
||||
s.edit(|d, _| {
|
||||
let sp = d.spread_mut(r).ok_or_else(|| bad("guide.deleteAll", "no such spread"))?;
|
||||
let mut n = 0;
|
||||
for pg in &mut sp.pages {
|
||||
n += pg.guides.len();
|
||||
pg.guides.clear();
|
||||
}
|
||||
Ok(json!({"deleted": n}))
|
||||
})
|
||||
}),
|
||||
cmd!(query "guide.list", "Guides", [], None, "{spread?} → [{page, index, orientation, position, spread}]", has_doc, |s, p| {
|
||||
let d = &s.doc()?.doc;
|
||||
let sp = d.spread(spread_param(p, "spread")).ok_or_else(|| bad("guide.list", "no such spread"))?;
|
||||
let mut out = Vec::new();
|
||||
for (pi, pg) in sp.pages.iter().enumerate() {
|
||||
for (i, g) in pg.guides.iter().enumerate() {
|
||||
out.push(json!({"page": pi, "index": i, "orientation": g.orientation, "position": g.position, "spread": g.spread}));
|
||||
}
|
||||
}
|
||||
Ok(Value::Array(out))
|
||||
}),
|
||||
cmd!(
|
||||
"layout.createGuides",
|
||||
"Create Guides…",
|
||||
["Layout"],
|
||||
None,
|
||||
"{rows?: 0, columns?: 0, rowGutter?: 12, columnGutter?: 12, fitTo?: margins|page, removeExisting?: false, spread?, page? (index in the spread; default all pages)}",
|
||||
has_doc,
|
||||
create_guides
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn orientation(p: &Value) -> Result<Orientation> {
|
||||
match str_param(p, "orientation") {
|
||||
Some("horizontal") => Ok(Orientation::Horizontal),
|
||||
Some("vertical") => Ok(Orientation::Vertical),
|
||||
_ => Err(bad("guide", "orientation: horizontal|vertical")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The page of a spread at a spread x coordinate (nearest page).
|
||||
fn page_at(d: &Document, r: SpreadRef, x: f64) -> usize {
|
||||
d.spread(r).and_then(|sp| sp.page_at_x(x)).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let o = orientation(p)?;
|
||||
let pos = p.get("position").and_then(Value::as_f64).ok_or_else(|| bad("guide.add", "missing position"))?;
|
||||
let r = spread_param(p, "spread");
|
||||
let spread_guide = p.get("spreadGuide").and_then(Value::as_bool).unwrap_or(false);
|
||||
let d = &s.doc()?.doc;
|
||||
let pi = match p.get("page").and_then(Value::as_u64) {
|
||||
Some(pi) => pi as usize,
|
||||
None if spread_guide => 0,
|
||||
None => {
|
||||
let x = match o {
|
||||
Orientation::Vertical => pos,
|
||||
Orientation::Horizontal => p.get("at").and_then(Value::as_f64).unwrap_or(0.0),
|
||||
};
|
||||
page_at(d, r, x)
|
||||
}
|
||||
};
|
||||
s.edit(|d, _| {
|
||||
let sp = d.spread_mut(r).ok_or_else(|| bad("guide.add", "no such spread"))?;
|
||||
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 });
|
||||
Ok(json!({"page": pi, "index": pg.guides.len() - 1}))
|
||||
})
|
||||
}
|
||||
|
||||
fn guide_ref(p: &Value, cmd: &str) -> Result<(SpreadRef, usize, usize)> {
|
||||
let pi = p.get("page").and_then(Value::as_u64).ok_or_else(|| bad(cmd, "missing page"))? as usize;
|
||||
let i = p.get("index").and_then(Value::as_u64).ok_or_else(|| bad(cmd, "missing index"))? as usize;
|
||||
Ok((spread_param(p, "spread"), pi, i))
|
||||
}
|
||||
|
||||
fn move_guide(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (r, pi, i) = guide_ref(p, "guide.move")?;
|
||||
let pos = p.get("position").and_then(Value::as_f64).ok_or_else(|| bad("guide.move", "missing position"))?;
|
||||
s.edit(|d, _| {
|
||||
let g = d
|
||||
.spread_mut(r)
|
||||
.and_then(|sp| sp.pages.get_mut(pi))
|
||||
.and_then(|pg| pg.guides.get_mut(i))
|
||||
.ok_or_else(|| bad("guide.move", "no such guide"))?;
|
||||
if g.locked {
|
||||
return Err(bad("guide.move", "the guide is locked"));
|
||||
}
|
||||
g.position = pos;
|
||||
Ok(Value::Null)
|
||||
})
|
||||
}
|
||||
|
||||
fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (r, pi, i) = guide_ref(p, "guide.delete")?;
|
||||
s.edit(|d, _| {
|
||||
let pg = d.spread_mut(r).and_then(|sp| sp.pages.get_mut(pi)).ok_or_else(|| bad("guide.delete", "no such page"))?;
|
||||
if i >= pg.guides.len() {
|
||||
return Err(bad("guide.delete", "no such guide"));
|
||||
}
|
||||
pg.guides.remove(i);
|
||||
Ok(Value::Null)
|
||||
})
|
||||
}
|
||||
|
||||
/// Evenly spaced rows/columns with gutters inside `area`: the guide positions.
|
||||
pub(crate) fn grid_positions(a: f64, b: f64, n: u32, gutter: f64) -> Vec<f64> {
|
||||
if n < 2 {
|
||||
return vec![];
|
||||
}
|
||||
let cell = ((b - a) - gutter * (n - 1) as f64) / n as f64;
|
||||
let mut v = Vec::new();
|
||||
for k in 1..n {
|
||||
let edge = a + k as f64 * cell + (k - 1) as f64 * gutter;
|
||||
v.push(edge);
|
||||
if gutter > 0.0 {
|
||||
v.push(edge + gutter);
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn create_guides(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let rows = p.get("rows").and_then(Value::as_u64).unwrap_or(0) as u32;
|
||||
let cols = p.get("columns").and_then(Value::as_u64).unwrap_or(0) as u32;
|
||||
let rg = p.get("rowGutter").and_then(Value::as_f64).unwrap_or(12.0);
|
||||
let cg = p.get("columnGutter").and_then(Value::as_f64).unwrap_or(12.0);
|
||||
let to_page = str_param(p, "fitTo") == Some("page");
|
||||
let remove = p.get("removeExisting").and_then(Value::as_bool).unwrap_or(false);
|
||||
let r = spread_param(p, "spread");
|
||||
let only = p.get("page").and_then(Value::as_u64).map(|v| v as usize);
|
||||
s.edit(|d, _| {
|
||||
let sp = d.spread_mut(r).ok_or_else(|| bad("layout.createGuides", "no such spread"))?;
|
||||
let mut n = 0;
|
||||
for (pi, pg) in sp.pages.iter_mut().enumerate() {
|
||||
if only.is_some_and(|o| o != pi) {
|
||||
continue;
|
||||
}
|
||||
if remove {
|
||||
pg.guides.clear();
|
||||
}
|
||||
let area: Rect = if to_page { pg.bounds() } else { pg.margin_rect() };
|
||||
for y in grid_positions(area.y0, area.y1, rows, rg) {
|
||||
pg.guides.push(Guide { orientation: Orientation::Horizontal, position: y, spread: false, locked: false });
|
||||
n += 1;
|
||||
}
|
||||
for x in grid_positions(area.x0, area.x1, cols, cg) {
|
||||
pg.guides.push(Guide { orientation: Orientation::Vertical, position: x, spread: false, locked: false });
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
Ok(json!({"guides": n}))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn guides_add_move_delete_and_grid() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"pages": 3})).unwrap();
|
||||
// Spread 1 holds pages 2–3 side by side from x = 0.
|
||||
let r = s.execute("guide.add", &json!({"spread": 1, "orientation": "vertical", "position": 100.0})).unwrap();
|
||||
assert_eq!(r["page"], 0);
|
||||
let r = s.execute("guide.add", &json!({"spread": 1, "orientation": "horizontal", "position": 200.0, "at": 700.0})).unwrap();
|
||||
assert_eq!(r["page"], 1);
|
||||
s.execute("guide.move", &json!({"spread": 1, "page": 1, "index": 0, "position": 250.0})).unwrap();
|
||||
let l = s.execute("guide.list", &json!({"spread": 1})).unwrap();
|
||||
assert_eq!(l.as_array().unwrap().len(), 2);
|
||||
assert_eq!(l[1]["position"], 250.0);
|
||||
s.execute("guide.delete", &json!({"spread": 1, "page": 0, "index": 0})).unwrap();
|
||||
assert_eq!(s.execute("guide.list", &json!({"spread": 1})).unwrap().as_array().unwrap().len(), 1);
|
||||
// 3 columns with 12 pt gutters inside the margins: 4 guides; 2 rows without gutter: 1 guide.
|
||||
let r = s.execute("layout.createGuides", &json!({"spread": 0, "columns": 3, "rows": 2, "rowGutter": 0.0})).unwrap();
|
||||
assert_eq!(r["guides"], 5);
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let pg = &d.spreads[0].pages[0];
|
||||
let m = pg.margin_rect();
|
||||
let v: Vec<f64> = pg.guides.iter().filter(|g| g.orientation == Orientation::Vertical).map(|g| g.position).collect();
|
||||
let cell = (m.width() - 24.0) / 3.0;
|
||||
assert!((v[0] - (m.x0 + cell)).abs() < 1e-9 && (v[1] - (m.x0 + cell + 12.0)).abs() < 1e-9);
|
||||
s.execute("guide.deleteAll", &json!({"spread": 0})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.spreads[0].pages[0].guides.is_empty());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.spreads[0].pages[0].guides.len(), 5);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ mod edit;
|
|||
mod export;
|
||||
mod file;
|
||||
mod find;
|
||||
mod guides;
|
||||
mod index;
|
||||
mod inspect;
|
||||
mod interactive;
|
||||
|
|
@ -130,6 +131,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
|||
v.extend(xref::specs());
|
||||
v.extend(index::specs());
|
||||
v.extend(anchored::specs());
|
||||
v.extend(guides::specs());
|
||||
v.extend(spelling::specs());
|
||||
v.extend(datamerge::specs());
|
||||
v.extend(interactive::specs());
|
||||
|
|
|
|||
|
|
@ -48,10 +48,13 @@ fn main() {
|
|||
);
|
||||
harness.input_mut().max_texture_side = Some(8192);
|
||||
READY.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
harness.run_steps(6);
|
||||
step_n(&mut harness, 6);
|
||||
for l in lines {
|
||||
if std::env::var_os("UI_SHOT_TRACE").is_some() {
|
||||
eprintln!("> {l}");
|
||||
}
|
||||
if let Some(p) = l.get("shot").and_then(|v| v.as_str()) {
|
||||
harness.run_steps(3);
|
||||
step_n(&mut harness, 3);
|
||||
match harness.render() {
|
||||
Ok(img) => {
|
||||
img.save(p).expect("save png");
|
||||
|
|
@ -60,20 +63,35 @@ fn main() {
|
|||
Err(e) => eprintln!("render failed: {e}"),
|
||||
}
|
||||
} else if let Some(n) = l.get("steps").and_then(|v| v.as_u64()) {
|
||||
harness.run_steps(n as usize);
|
||||
step_n(&mut harness, n as usize);
|
||||
} else if let Some(m) = l.get("method").and_then(|v| v.as_str()) {
|
||||
let (req, reply) = ControlRequest::new(m, l.get("params").cloned().unwrap_or_default());
|
||||
tx.send(req).expect("send");
|
||||
// Pointer/keyboard input is injected one event per frame.
|
||||
for _ in 0..40 {
|
||||
harness.step();
|
||||
step(&mut harness);
|
||||
if let Ok(r) = reply.try_recv() {
|
||||
let s = r.to_string();
|
||||
println!("{m}: {}", &s[..s.len().min(300)]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
harness.run_steps(2);
|
||||
step_n(&mut harness, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One frame, with the app's synthetic input (`ui.click`, `ui.drag`, `ui.key`) injected like the
|
||||
/// windowed app's raw-input hook does.
|
||||
fn step(harness: &mut egui_kittest::Harness<'_, DesignApp>) {
|
||||
let mut raw = std::mem::take(harness.input_mut());
|
||||
harness.state_mut().raw_input_hook(&mut raw);
|
||||
*harness.input_mut() = raw;
|
||||
harness.step();
|
||||
}
|
||||
|
||||
fn step_n(harness: &mut egui_kittest::Harness<'_, DesignApp>, n: usize) {
|
||||
for _ in 0..n {
|
||||
step(harness);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,6 +200,16 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
if !preview {
|
||||
draw_guides(app, &painter, &xf, &doc, &layout, &t);
|
||||
if let Some(g) = ui.data(|d| d.get_temp::<GuideDrag>(egui::Id::new(GUIDE_DRAG)))
|
||||
&& let Some(p) = g.at
|
||||
{
|
||||
let col = Color32::from_rgb(74, 227, 255);
|
||||
let (a, b) = match g.orientation {
|
||||
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));
|
||||
}
|
||||
draw_frames(app, &painter, &xf, &doc, &layout);
|
||||
if app.ui.hidden_characters {
|
||||
draw_hidden_characters(app, &painter, &xf, &doc, &layout);
|
||||
|
|
@ -410,9 +420,14 @@ fn draw_guides(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Document
|
|||
}
|
||||
}
|
||||
for g in &p.guides {
|
||||
// Spread guides cross the pasteboard; page guides their page.
|
||||
let span = if g.spread { layout.pasteboard } else { pr };
|
||||
let (a, bb) = match g.orientation {
|
||||
designcraft_doc::Orientation::Horizontal => (Point::new(pr.x0, g.position + off.y), Point::new(pr.x1, g.position + off.y)),
|
||||
designcraft_doc::Orientation::Vertical => (Point::new(g.position + off.x, pr.y0), Point::new(g.position + off.x, pr.y1)),
|
||||
designcraft_doc::Orientation::Horizontal => (Point::new(span.x0, g.position + off.y), Point::new(span.x1, g.position + off.y)),
|
||||
designcraft_doc::Orientation::Vertical => {
|
||||
let span = if g.spread { slot.bounds.inflate(0.0, 36.0) } else { pr };
|
||||
(Point::new(g.position + off.x, span.y0), Point::new(g.position + off.x, span.y1))
|
||||
}
|
||||
};
|
||||
painter.line_segment([xf.to_screen(a), xf.to_screen(bb)], Stroke::new(hair(painter), Color32::from_rgb(74, 227, 255)));
|
||||
}
|
||||
|
|
@ -1085,6 +1100,9 @@ fn handle_input(app: &mut DesignApp, ui: &mut egui::Ui, resp: &egui::Response, r
|
|||
}
|
||||
return;
|
||||
}
|
||||
if !space && guide_drag(app, ui, resp, rect, &xf) {
|
||||
return;
|
||||
}
|
||||
let m = ui.input(|i| mods(i, space));
|
||||
let vi = app.view_info();
|
||||
let pos = |p: Pos2| xf.to_canvas(p);
|
||||
|
|
@ -1240,3 +1258,129 @@ pub fn selected_text(app: &DesignApp) -> Option<String> {
|
|||
fn hair(painter: &egui::Painter) -> f32 {
|
||||
1.0 / painter.ctx().pixels_per_point()
|
||||
}
|
||||
|
||||
const GUIDE_DRAG: &str = "canvas_guide_drag";
|
||||
|
||||
/// A ruler guide being dragged: out of a ruler (`from` None) or an existing one.
|
||||
#[derive(Clone, Debug)]
|
||||
struct GuideDrag {
|
||||
orientation: designcraft_doc::Orientation,
|
||||
from: Option<(designcraft_doc::SpreadRef, usize, usize)>,
|
||||
/// Pointer position (screen).
|
||||
at: Option<Pos2>,
|
||||
}
|
||||
|
||||
/// The guide under a screen point (within 3 px), as (spread, page, index, orientation).
|
||||
fn guide_at(app: &DesignApp, xf: &Xf, p: Pos2) -> Option<(designcraft_doc::SpreadRef, usize, usize, designcraft_doc::Orientation)> {
|
||||
let st = app.session.active()?;
|
||||
let layout = CanvasLayout::new(&st.doc, st.editing_parents);
|
||||
let cp = xf.to_canvas(p);
|
||||
let tol = 3.0 / xf.zoom;
|
||||
for slot in &layout.slots {
|
||||
let sp = st.doc.spread(slot.spread)?;
|
||||
let local = cp - slot.offset;
|
||||
for (pi, pg) in sp.pages.iter().enumerate() {
|
||||
let pr = if pg.guides.iter().any(|g| g.spread) { layout.pasteboard - slot.offset } else { pg.bounds() };
|
||||
for (gi, g) in pg.guides.iter().enumerate() {
|
||||
let r = if g.spread { pr } else { pg.bounds() };
|
||||
let hit = match g.orientation {
|
||||
designcraft_doc::Orientation::Horizontal => (local.y - g.position).abs() <= tol && local.x >= r.x0 && local.x <= r.x1,
|
||||
designcraft_doc::Orientation::Vertical => (local.x - g.position).abs() <= tol && local.y >= r.y0 - tol && local.y <= r.y1 + tol,
|
||||
};
|
||||
if hit && !g.locked {
|
||||
return Some((slot.spread, pi, gi, g.orientation));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Ruler guides: drag one out of a ruler, drag an existing one (Selection tools, not over an
|
||||
/// object) to move it, drop it back on a ruler to delete it. Returns true while it owns the pointer.
|
||||
fn guide_drag(app: &mut DesignApp, ui: &mut egui::Ui, resp: &egui::Response, rect: Rect, xf: &Xf) -> bool {
|
||||
let id = egui::Id::new(GUIDE_DRAG);
|
||||
let (pressed, released, origin, latest) =
|
||||
ui.input(|i| (i.pointer.primary_pressed(), i.pointer.primary_released(), i.pointer.press_origin(), i.pointer.latest_pos()));
|
||||
let mut drag: Option<GuideDrag> = ui.data(|d| d.get_temp(id));
|
||||
if drag.is_none() && pressed && resp.hovered() && app.ui.guides && !app.ui.guides_locked {
|
||||
let Some(o) = origin else { return false };
|
||||
let full = resp.rect;
|
||||
if full.contains(o) && !rect.contains(o) {
|
||||
// In a ruler (not the corner).
|
||||
let orientation = if o.y < rect.min.y && o.x >= rect.min.x {
|
||||
Some(designcraft_doc::Orientation::Horizontal)
|
||||
} else if o.x < rect.min.x && o.y >= rect.min.y {
|
||||
Some(designcraft_doc::Orientation::Vertical)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(orientation) = orientation {
|
||||
drag = Some(GuideDrag { orientation, from: None, at: Some(o) });
|
||||
}
|
||||
} else if rect.contains(o)
|
||||
&& matches!(app.session.tool_id(), "selection" | "directSelection")
|
||||
&& let Some((r, pi, gi, orientation)) = guide_at(app, xf, o)
|
||||
{
|
||||
// Objects take precedence over the guides behind them.
|
||||
let over_item = app.session.active().is_some_and(|st| {
|
||||
let layout = CanvasLayout::new(&st.doc, st.editing_parents);
|
||||
layout.spread_at(xf.to_canvas(o)).is_some_and(|(sr, p)| match sr {
|
||||
designcraft_doc::SpreadRef::Doc(si) => st.doc.hit_item(si, p, 3.0 / xf.zoom).is_some(),
|
||||
_ => false,
|
||||
})
|
||||
});
|
||||
if !over_item {
|
||||
drag = Some(GuideDrag { orientation, from: Some((r, pi, gi)), at: Some(o) });
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(mut g) = drag else { return false };
|
||||
if let Some(p) = latest {
|
||||
g.at = Some(p);
|
||||
}
|
||||
if !released {
|
||||
ui.ctx().set_cursor_icon(match g.orientation {
|
||||
designcraft_doc::Orientation::Horizontal => egui::CursorIcon::ResizeVertical,
|
||||
designcraft_doc::Orientation::Vertical => egui::CursorIcon::ResizeHorizontal,
|
||||
});
|
||||
ui.data_mut(|d| d.insert_temp(id, g));
|
||||
return true;
|
||||
}
|
||||
ui.data_mut(|d| d.remove::<GuideDrag>(id));
|
||||
let Some(p) = g.at else { return true };
|
||||
let on_canvas = rect.contains(p);
|
||||
let target = app.session.active().and_then(|st| {
|
||||
let layout = CanvasLayout::new(&st.doc, st.editing_parents);
|
||||
let (r, sp) = layout.spread_at(xf.to_canvas(p))?;
|
||||
let spread = st.doc.spread(r)?;
|
||||
let on_page = spread.pages.iter().position(|pg| pg.bounds().contains(sp));
|
||||
Some((r, sp, on_page))
|
||||
});
|
||||
let pos_of = |sp: Point| match g.orientation {
|
||||
designcraft_doc::Orientation::Horizontal => sp.y,
|
||||
designcraft_doc::Orientation::Vertical => sp.x,
|
||||
};
|
||||
let orient = match g.orientation {
|
||||
designcraft_doc::Orientation::Horizontal => "horizontal",
|
||||
designcraft_doc::Orientation::Vertical => "vertical",
|
||||
};
|
||||
match (g.from, on_canvas, target) {
|
||||
// Dropped back on a ruler: delete.
|
||||
(Some((r, pi, gi)), false, _) => {
|
||||
let _ = app.run("guide.delete", json!({"spread": r, "page": pi, "index": gi}));
|
||||
}
|
||||
(Some((r, pi, gi)), true, Some((tr, sp, _))) if tr == r => {
|
||||
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()});
|
||||
if let Some(pi) = on_page {
|
||||
params["page"] = json!(pi);
|
||||
}
|
||||
let _ = app.run("guide.add", params);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
|
|||
("app.insertTableDialog", "Create Table…", None, "{} — Insert Table dialog (body/header/footer rows, columns)"),
|
||||
("app.footnoteOptionsDialog", "Document Footnote Options…", None, "{} — numbering, formatting and layout of footnotes"),
|
||||
("app.insertXrefDialog", "Insert Cross-Reference…", None, "{} — New Cross-Reference dialog (paragraph or text anchor, format)"),
|
||||
("app.deleteAllGuides", "Delete All Guides on Spread", None, "{} — the spread in view"),
|
||||
("app.deletePage", "Delete Page", None, "{} — deletes the page in view"),
|
||||
("app.duplicateSpread", "Duplicate Spread", None, "{} — duplicates the spread in view (pages and items)"),
|
||||
("app.tablePanel", "Table Panel", Some("Shift+F9"), "{}"),
|
||||
|
|
@ -132,6 +133,7 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
|||
"cmd:layout.parents.new",
|
||||
"<",
|
||||
"cmd:layout.marginsAndColumns",
|
||||
"cmd:layout.createGuides",
|
||||
"-",
|
||||
"ui:view.goToPage",
|
||||
"-",
|
||||
|
|
@ -345,8 +347,12 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
|||
"ui:view.textThreads",
|
||||
"ui:view.hiddenCharacters",
|
||||
"-",
|
||||
">Grids & Guides",
|
||||
"ui:view.guides",
|
||||
"ui:view.baselineGrid",
|
||||
"-",
|
||||
"ui:app.deleteAllGuides",
|
||||
"<",
|
||||
],
|
||||
),
|
||||
(
|
||||
|
|
@ -456,6 +462,12 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
|||
app.ui.dialog = Some(crate::dialogs::Dialog::new("insertXref", json!({})));
|
||||
Ok(Value::Null)
|
||||
}
|
||||
"app.deleteAllGuides" => {
|
||||
let Some(st) = app.session.active() else { return Some(Err("no document open".into())) };
|
||||
let page = crate::canvas::current_page(app).unwrap_or(0);
|
||||
let spread = if st.editing_parents { json!({"kind": "parent", "index": 0}) } else { json!(st.doc.page_loc(page).map_or(0, |l| l.0)) };
|
||||
return Some(app.run("guide.deleteAll", json!({"spread": spread})));
|
||||
}
|
||||
"app.deletePage" | "app.duplicateSpread" => {
|
||||
let Some(st) = app.session.active() else { return Some(Err("no document open".into())) };
|
||||
let page = crate::canvas::current_page(app).unwrap_or(0);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ pub const HEIGHT: f32 = 36.0;
|
|||
|
||||
/// Show the bar under `sel` (screen rect of the selection), kept inside `canvas`.
|
||||
pub fn show(app: &mut DesignApp, ctx: &egui::Context, sel: Rect, canvas: Rect) {
|
||||
if !app.ui.task_bar || ctx.input(|i| i.pointer.any_down() && !ctx.is_pointer_over_egui()) {
|
||||
// (Two separate context calls: `is_pointer_over_egui` inside an `input` closure deadlocks.)
|
||||
if !app.ui.task_bar || (ctx.input(|i| i.pointer.any_down()) && !ctx.is_pointer_over_egui()) {
|
||||
return;
|
||||
}
|
||||
let Some(st) = app.session.active() else { return };
|
||||
|
|
|
|||
Loading…
Reference in New Issue