V: Power Zoom by press-and-hold with the Hand tool

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-03 09:10:59 -04:00
1 parent 8ac240889f
commit 031d099b3a
4 files changed
+67 -4

No files matched your search

+1 -1
View File
@@ -87,7 +87,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- Move table rows and columns (`table.moveRow`, `table.moveColumn`, Table panel buttons; dragging on the canvas isn't there yet).
- Object States (`states.create/show/rename/release/list`, Object States panel): multi-state objects show one state on screen and in PDF.
- Buttons (`button.set/clear/list`, Buttons and Forms panel): go to page / next / previous / first / last / URL as PDF link annotations. Form fields need AcroForm support the PDF writer lacks.
- Power Zoom (Hand tool, Alt-press): zoom out to the spread, aim the red view rectangle, release to zoom back in there. InDesign triggers it by press-and-hold instead.
- Power Zoom (Hand tool: press and hold still for half a second, or Alt-press): zoom out to the spread, aim the red view rectangle, release to zoom back in there.
- Graphic cells (`table.placeGraphic`, `table.textCell`, Table › Convert Cell Type, Table panel): images in table cells, fitted or filled, clipped to the cell on screen and in PDF.
- Live Distribute (Space while dragging a selection handle; `transform.resize {distribute}`): objects keep their size and spread with the bounds.
- Liquid Layout (`liquid.pageRule/object`, `guide.liquid`, `layout.createAlternate`, Liquid Layout panel): scale / re-center / guide-based / object-based page rules applied on page resize and Document Setup; dashed liquid guides; alternate layouts as resized page copies in a named section. Alternate layouts aren't linked to their source.
+20
View File
@@ -1362,6 +1362,26 @@ fn handle_input(app: &mut DesignApp, ui: &mut egui::Ui, resp: &egui::Response, r
{
events.push(PointerEvent { kind: PointerKind::Drag, pos: pos(p), mods: m });
}
// Power Zoom: holding the Hand tool still for half a second turns the press into one.
let hold_id = egui::Id::new(("canvas_hold", app.pane));
if down && !released && app.session.tool_id() == "hand" && !m.alt {
let (now, origin_pos) = ui.input(|i| (i.time, i.pointer.press_origin()));
let (start, fired): (f64, bool) = ui.data(|d| d.get_temp(hold_id)).unwrap_or((now, false));
let still = origin_pos.zip(latest).is_some_and(|(o, l)| (o - l).length() < 3.0);
if !fired && still && now - start >= 0.5 {
let p = pos(latest.unwrap_or(rect.center()));
events.push(PointerEvent { kind: PointerKind::Up, pos: p, mods: m });
events.push(PointerEvent { kind: PointerKind::Down, pos: p, mods: Mods { alt: true, ..m } });
ui.data_mut(|d| d.insert_temp(hold_id, (start, true)));
} else {
ui.data_mut(|d| d.insert_temp(hold_id, (start, fired)));
if !fired {
ui.ctx().request_repaint_after(std::time::Duration::from_millis(100));
}
}
} else {
ui.data_mut(|d| d.remove::<(f64, bool)>(hold_id));
}
if down && released {
let p = latest.unwrap_or(rect.center());
events.push(PointerEvent { kind: PointerKind::Up, pos: pos(p), mods: m });
+43
View File
@@ -2032,6 +2032,49 @@ mod tests {
assert_eq!(run_ui(&mut app, "view.rotateSpread", &json!({"angle": 0})).unwrap().unwrap(), json!(0));
}
#[test]
fn hand_tool_hold_starts_power_zoom() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.session.execute("file.new", &json!({})).unwrap();
let ctx = egui::Context::default();
let frame = |app: &mut crate::DesignApp, t: f64, events: Vec<egui::Event>| {
let input = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0))),
time: Some(t),
events,
..Default::default()
};
let mut out = ctx.run_ui(input, |ui| {
app.logic(&ui.ctx().clone());
app.ui(ui);
});
out.textures_delta.clear();
};
frame(&mut app, 0.0, vec![]);
frame(&mut app, 0.05, vec![]);
app.select_tool("hand");
let r = app.canvas_rect.unwrap();
let p = r.center();
let z0 = app.view().unwrap().zoom;
frame(&mut app, 0.1, vec![egui::Event::PointerMoved(p)]);
frame(
&mut app,
0.2,
vec![egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() }],
);
frame(&mut app, 0.4, vec![]);
assert!(app.power_zoom.is_none(), "not yet");
frame(&mut app, 0.8, vec![]);
assert!(app.power_zoom.is_some(), "holding still starts Power Zoom");
frame(
&mut app,
0.9,
vec![egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() }],
);
assert!(app.power_zoom.is_none());
assert!((app.view().unwrap().zoom - z0).abs() < 1e-9, "back at the zoom it started from");
}
#[test]
fn panels_float_and_dock() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
+3 -3
View File
@@ -11,7 +11,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|---|---|---|---|---|---|
| P0 | 102 | 100 | 2 | 0 | 99% |
| P1 | 97 | 94 | 3 | 0 | 98% |
| P2 | 76 | 50 | 26 | 0 | 83% |
| P2 | 76 | 51 | 25 | 0 | 84% |
| Area | Parity |
|---|---|
@@ -31,7 +31,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
| Interactivity & digital | 90% |
| Output & production | 82% |
| XML & automation | 83% |
| View & navigation | 95% |
| View & navigation | 96% |
| Undo, history, saving | 100% |
| Accessibility | 90% |
@@ -297,7 +297,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
| XML & automation | Snippets | P1 | D |
| XML & automation | Object Library | P2 | D |
| View & navigation | Zoom commands | P0 | D |
| View & navigation | Power Zoom | P2 | P |
| View & navigation | Power Zoom | P2 | D |
| View & navigation | Animated/scrubby zoom | P1 | D |
| View & navigation | Screen modes | P0 | D |
| View & navigation | Overprint preview | P1 | D |