mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 20:56:47 -06:00
Merge PR #1044: View: Presentation shows one spread fitted to the screen; arrow keys and clicks go through the spreads
Conflicts: - crates/ui-egui/src/menus.rs: kept #503's Esc branch first, then #1044's spread-stepping keys.
This commit is contained in:
@@ -460,6 +460,7 @@ Newest first.
|
||||
- 2026-10-15: Tab, Shift+Tab and Presentation hide the icon column's panel flyout and the classic floating panels too, as InDesign hides floating panels.
|
||||
- 2026-10-15: Layers panel: an object's row is renamed with a slow double-click (a second click on the selected row), as in InDesign; a quick double-click only selects it.
|
||||
- **2026-10-15** · New Workspace is InDesign's: Name, and Capture › Panel Locations and Menu Customization (the items hidden with Edit › Menus), both on by default; a workspace saved without panel locations leaves the panels where they are. No readiness percentage change.
|
||||
- 2026-10-15: Presentation shows one spread at a time, as large as the screen allows; the arrow keys, Page Up/Down, Home, End, a click and Shift-click go through the spreads.
|
||||
- 2026-10-15: Layout › First Page, Previous Page, Next Page, Last Page, Next Spread and Previous Spread, with InDesign's shortcuts (Shift+Page Up/Down, Shift+Cmd+Page Up/Down, Alt+Page Up/Down).
|
||||
- **2026-10-14** · The Type tool draws a new text frame over a locked empty frame, as InDesign does, instead of turning the locked frame into a text frame. No readiness percentage change.
|
||||
- **2026-10-14** · 100% black shows as pure black on screen by default, as InDesign's Appearance of Black does out of the box (Display All Blacks as Rich Black); Preferences › Appearance of Black › Display All Blacks Accurately still shows it as the dark grey it prints, and a saved choice is kept. No readiness percentage change.
|
||||
|
||||
@@ -249,6 +249,22 @@ pub fn go_to_page(app: &mut DesignApp, abs: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Presentation: the spread in view, centred and as large as `rect` allows.
|
||||
fn present_fit(app: &mut DesignApp, rect: Rect) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let layout = CanvasLayout::new(&st.doc, st.editing_parents);
|
||||
let Some(b) = current_slot(app, &layout).and_then(|i| layout.slots.get(i)).map(|s| s.bounds) else { return };
|
||||
if b.width() <= 0.0 || b.height() <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let zoom = (rect.width() as f64 / b.width()).min(rect.height() as f64 / b.height()).clamp(0.05, 40.0);
|
||||
let origin = Point::new(b.center().x - rect.width() as f64 / 2.0 / zoom, b.center().y - rect.height() as f64 / 2.0 / zoom);
|
||||
if let Some(v) = app.view_mut() {
|
||||
v.zoom = zoom;
|
||||
v.origin = origin;
|
||||
}
|
||||
}
|
||||
|
||||
/// Centre the spread `delta` spreads after (or before) the one in view (keeps zoom); stops at the
|
||||
/// first and last spread. Parent spreads while they are being edited.
|
||||
pub fn step_spread(app: &mut DesignApp, delta: isize) {
|
||||
@@ -430,10 +446,25 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let rect = view_rect(screen, rot);
|
||||
app.canvas_rect = Some(rect);
|
||||
let resp = ui.allocate_rect(full, Sense::click_and_drag());
|
||||
if !app.view().is_some_and(|v| v.fitted) {
|
||||
fit(app, rect, "spread");
|
||||
if presenting {
|
||||
// One spread at a time, as large as the screen allows; a click shows the next one,
|
||||
// Shift-click the previous one. Tools and selection don't see the input.
|
||||
present_fit(app, rect);
|
||||
if resp.clicked() {
|
||||
// The release that made the click says whether Shift was down.
|
||||
let shift = ui.input(|i| {
|
||||
i.modifiers.shift
|
||||
|| i.events.iter().any(|e| matches!(e, egui::Event::PointerButton { pressed: false, modifiers, .. } if modifiers.shift))
|
||||
});
|
||||
step_spread(app, if shift { -1 } else { 1 });
|
||||
present_fit(app, rect);
|
||||
}
|
||||
} else {
|
||||
if !app.view().is_some_and(|v| v.fitted) {
|
||||
fit(app, rect, "spread");
|
||||
}
|
||||
handle_input(app, ui, &resp, rect);
|
||||
}
|
||||
handle_input(app, ui, &resp, rect);
|
||||
if bars {
|
||||
scrollbars(app, ui, outer, screen, rect, &t);
|
||||
}
|
||||
@@ -441,7 +472,10 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let Some(&v) = app.view() else { return };
|
||||
let xf = Xf::new(rect, &v);
|
||||
let doc = st.doc.clone();
|
||||
let layout = CanvasLayout::new(&doc, st.editing_parents);
|
||||
let mut layout = CanvasLayout::new(&doc, st.editing_parents);
|
||||
if presenting && let Some(cur) = current_slot(app, &layout) {
|
||||
layout.slots = layout.slots.get(cur).copied().into_iter().collect();
|
||||
}
|
||||
let painter = ui.painter_at(screen);
|
||||
let preview = matches!(app.ui.screen_mode, ScreenMode::Preview | ScreenMode::Presentation);
|
||||
let bg = if app.ui.screen_mode == ScreenMode::Presentation { Color32::BLACK } else { t.pasteboard };
|
||||
@@ -521,6 +555,9 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
draw_dynamic_spelling(app, ui.ctx(), &painter, &xf, &doc, &layout);
|
||||
}
|
||||
}
|
||||
if presenting {
|
||||
return;
|
||||
}
|
||||
let sel_rect = draw_selection(app, &painter, &xf, &doc, &layout);
|
||||
if !preview {
|
||||
// Content grabbers: the selected frame's and the hovered one's (Selection tool).
|
||||
|
||||
@@ -2995,6 +2995,21 @@ pub fn shortcuts(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
let _ = app.run("view.togglePresentation", json!({}));
|
||||
continue;
|
||||
}
|
||||
// Presentation goes through the spreads with the arrow keys, Page Up/Down, Home and End.
|
||||
if app.ui.screen_mode == ScreenMode::Presentation && !modifiers.command && !modifiers.alt {
|
||||
let step = match key {
|
||||
egui::Key::ArrowRight | egui::Key::ArrowDown | egui::Key::PageDown => Some(1),
|
||||
egui::Key::ArrowLeft | egui::Key::ArrowUp | egui::Key::PageUp => Some(-1),
|
||||
egui::Key::Home => Some(isize::MIN),
|
||||
egui::Key::End => Some(isize::MAX),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(step) = step {
|
||||
ctx.input_mut(|i| i.consume_key(modifiers, key));
|
||||
crate::canvas::step_spread(app, step);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Command shortcuts.
|
||||
let mut fired: Option<(String, bool)> = None;
|
||||
let all = effective_shortcuts(app);
|
||||
@@ -4181,6 +4196,99 @@ mod tests {
|
||||
assert_eq!(v.zoom, zoom);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presentation_shows_one_spread_and_goes_through_them_with_keys_and_clicks() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.session.execute("file.new", &json!({"pages": 7, "facingPages": true})).unwrap();
|
||||
app.session.execute("frame.create", &json!({"rect": [100, 100, 300, 300]})).unwrap();
|
||||
let id = app.session.active().unwrap().selection.items[0];
|
||||
let ctx = egui::Context::default();
|
||||
let frame = |app: &mut crate::DesignApp, 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))),
|
||||
events,
|
||||
..Default::default()
|
||||
};
|
||||
let mut out = ctx.run_ui(input, |ui| {
|
||||
app.logic(&ui.ctx().clone());
|
||||
app.ui(ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
let key = |k: egui::Key| {
|
||||
vec![
|
||||
egui::Event::Key { key: k, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() },
|
||||
egui::Event::Key { key: k, physical_key: None, pressed: false, repeat: false, modifiers: Default::default() },
|
||||
]
|
||||
};
|
||||
let click = |pos: egui::Pos2, modifiers: egui::Modifiers| {
|
||||
vec![
|
||||
egui::Event::PointerMoved(pos),
|
||||
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, modifiers },
|
||||
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers },
|
||||
]
|
||||
};
|
||||
let spread = |app: &crate::DesignApp| {
|
||||
let st = app.session.active().unwrap();
|
||||
crate::canvas::current_slot(app, &designcraft_tools::layout::CanvasLayout::new(&st.doc, false)).unwrap()
|
||||
};
|
||||
// The spread fills the screen: as wide or as tall as it, centred.
|
||||
let fills = |app: &crate::DesignApp| {
|
||||
let st = app.session.active().unwrap();
|
||||
let b = designcraft_tools::layout::CanvasLayout::new(&st.doc, false).slots[spread(app)].bounds;
|
||||
let (v, r) = (*app.view().unwrap(), app.canvas_rect.unwrap());
|
||||
let (w, h) = (b.width() * v.zoom, b.height() * v.zoom);
|
||||
let x0 = (b.x0 - v.origin.x) * v.zoom;
|
||||
let y0 = (b.y0 - v.origin.y) * v.zoom;
|
||||
((w - r.width() as f64).abs() < 0.01 || (h - r.height() as f64).abs() < 0.01)
|
||||
&& w <= r.width() as f64 + 0.01
|
||||
&& h <= r.height() as f64 + 0.01
|
||||
&& (x0 * 2.0 + w - r.width() as f64).abs() < 0.01
|
||||
&& (y0 * 2.0 + h - r.height() as f64).abs() < 0.01
|
||||
};
|
||||
frame(&mut app, vec![]);
|
||||
frame(&mut app, vec![]);
|
||||
app.run("view.screenMode", json!({"mode": "presentation"})).unwrap();
|
||||
frame(&mut app, vec![]);
|
||||
frame(&mut app, vec![]);
|
||||
assert_eq!(app.canvas_rect.unwrap().size(), egui::vec2(1400.0, 900.0), "the whole window");
|
||||
assert_eq!(spread(&app), 0);
|
||||
assert!(fills(&app));
|
||||
let bounds = app.session.active().unwrap().doc.item(id).unwrap().bounds();
|
||||
frame(&mut app, key(egui::Key::ArrowRight));
|
||||
frame(&mut app, vec![]);
|
||||
assert_eq!(spread(&app), 1, "Right Arrow shows the next spread");
|
||||
assert!(fills(&app));
|
||||
assert_eq!(app.session.active().unwrap().doc.item(id).unwrap().bounds(), bounds, "the arrow doesn't nudge the selection");
|
||||
frame(&mut app, key(egui::Key::PageDown));
|
||||
assert_eq!(spread(&app), 2, "Page Down");
|
||||
frame(&mut app, key(egui::Key::ArrowLeft));
|
||||
assert_eq!(spread(&app), 1, "Left Arrow");
|
||||
frame(&mut app, key(egui::Key::End));
|
||||
assert_eq!(spread(&app), 3, "End");
|
||||
frame(&mut app, key(egui::Key::ArrowRight));
|
||||
assert_eq!(spread(&app), 3, "the last spread stays");
|
||||
frame(&mut app, key(egui::Key::Home));
|
||||
assert_eq!(spread(&app), 0, "Home");
|
||||
frame(&mut app, key(egui::Key::PageUp));
|
||||
assert_eq!(spread(&app), 0, "the first spread stays");
|
||||
let c = app.canvas_rect.unwrap().center();
|
||||
frame(&mut app, click(c, egui::Modifiers::NONE));
|
||||
frame(&mut app, vec![]);
|
||||
assert_eq!(spread(&app), 1, "a click shows the next spread");
|
||||
assert!(fills(&app));
|
||||
frame(&mut app, click(c, egui::Modifiers::SHIFT));
|
||||
frame(&mut app, vec![]);
|
||||
assert_eq!(spread(&app), 0, "Shift-click the previous one");
|
||||
assert_eq!(app.session.active().unwrap().selection.items, vec![id], "clicks don't select");
|
||||
assert_eq!(app.session.tool_id(), "selection");
|
||||
// Out of Presentation, the arrow keys nudge again.
|
||||
app.run("view.screenMode", json!({"mode": "normal"})).unwrap();
|
||||
frame(&mut app, vec![]);
|
||||
frame(&mut app, key(egui::Key::ArrowRight));
|
||||
assert_ne!(app.session.active().unwrap().doc.item(id).unwrap().bounds(), bounds);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_shortcuts_work_after_drawing_on_the_canvas() {
|
||||
// Clicking the canvas gives it keyboard focus; single-key tool shortcuts must still switch
|
||||
|
||||
Reference in New Issue
Block a user