Merge PR #846: Canvas: the Format and Animation Painter override the cursor (#830)

Conflicts: crates/ui-egui/src/canvas.rs (cursor match: kept #806's resize_cursor arm inside #846's painter-armed wrapper)
This commit is contained in:
aiongg
2026-10-24 06:16:18 +00:00
2 changed files with 59 additions and 21 deletions
+1
View File
@@ -171,6 +171,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| 2026-10-15 | A `.pptx` slide that can't be read is reported, not dropped in silence: `lostSlides` from `file.open`, a message and a repaired copy in the app, a warning from `deckcraft-cli` (#203) |
| 2026-10-16 | AutoFormat as you type: ordinals (1st, 22nd, 103rd) get superscript suffixes after a space or punctuation; Tools ▸ AutoCorrect and `text.autocorrect {ordinals}` turn it off |
| 2026-10-16 | A PowerPoint Show (`.ppsx`) opened from the command line or the file association starts its slide show, as in PowerPoint; File › Open still opens it for editing (#862) |
| 2026-10-15 | Canvas: the Format and Animation Painter override the cursor to show the tool is armed (#830) |
| 2026-10-15 | PDF export and rendering: `anchorCtr` centres the drawn text block — right-aligned text no longer ran past the box edge (#736) |
| 2026-10-14 | connected MCP rejects unrelated, malformed, incomplete and oversized replies without replay; buffered unsolicited bytes force a fresh connection before sending (#718) |
| 2026-10-14 | Empty bulleted and numbered paragraphs show gray markers while editing, including on first click and after Enter. Markers use the list style and stay out of slide shows and exports. |
+58 -21
View File
@@ -889,29 +889,35 @@ fn pointer(app: &mut SlideApp, ui: &Ui, resp: &egui::Response, xf: Xf) {
if let Err(e) = r {
app.set_status(e.to_string());
}
// Cursor.
let cursor = match (&app.session.tool.kind, &app.session.tool.hover) {
(ToolKind::Shape { .. }, _) => egui::CursorIcon::Crosshair,
(ToolKind::TextBox, _) => egui::CursorIcon::Text,
(ToolKind::Ink { .. }, _) => egui::CursorIcon::Crosshair,
(_, Some(Hit::Resize { id, handle })) => resize_cursor(&app.session, *id, *handle),
(_, Some(Hit::Rotate { .. })) => egui::CursorIcon::Alias,
(_, Some(Hit::TableColumn { .. })) => egui::CursorIcon::ResizeColumn,
(_, Some(Hit::TableRow { .. })) => egui::CursorIcon::ResizeRow,
(_, Some(Hit::Adjust { .. } | Hit::Warp { .. } | Hit::LineEnd { .. })) => egui::CursorIcon::PointingHand,
(_, Some(Hit::Shape { text: true, .. })) => {
if app.session.active().is_some_and(|d| d.selection.text.is_some()) {
egui::CursorIcon::Text
} else {
egui::CursorIcon::Move
// Cursor. The Format and Animation Painters override everything: their whole point is to
// tell the user the tool is armed and waiting for a click (#830).
let painter_armed = app.session.painter.is_some() || app.session.anim_painter.is_some();
let cursor = if painter_armed {
egui::CursorIcon::Copy
} else {
match (&app.session.tool.kind, &app.session.tool.hover) {
(ToolKind::Shape { .. }, _) => egui::CursorIcon::Crosshair,
(ToolKind::TextBox, _) => egui::CursorIcon::Text,
(ToolKind::Ink { .. }, _) => egui::CursorIcon::Crosshair,
(_, Some(Hit::Resize { id, handle })) => resize_cursor(&app.session, *id, *handle),
(_, Some(Hit::Rotate { .. })) => egui::CursorIcon::Alias,
(_, Some(Hit::TableColumn { .. })) => egui::CursorIcon::ResizeColumn,
(_, Some(Hit::TableRow { .. })) => egui::CursorIcon::ResizeRow,
(_, Some(Hit::Adjust { .. } | Hit::Warp { .. } | Hit::LineEnd { .. })) => egui::CursorIcon::PointingHand,
(_, Some(Hit::Shape { text: true, .. })) => {
if app.session.active().is_some_and(|d| d.selection.text.is_some()) {
egui::CursorIcon::Text
} else {
egui::CursorIcon::Move
}
}
(_, Some(Hit::Shape { .. } | Hit::Frame { .. })) => egui::CursorIcon::Move,
_ => match app.session.tool.guide_drag.map(|g| g.0).or(app.session.tool.guide_hover) {
Some(i) if app.session.active().and_then(|d| d.doc.guides.get(i)).is_some_and(|g| g.vertical) => egui::CursorIcon::ResizeHorizontal,
Some(_) => egui::CursorIcon::ResizeVertical,
None => egui::CursorIcon::Default,
},
}
(_, Some(Hit::Shape { .. } | Hit::Frame { .. })) => egui::CursorIcon::Move,
_ => match app.session.tool.guide_drag.map(|g| g.0).or(app.session.tool.guide_hover) {
Some(i) if app.session.active().and_then(|d| d.doc.guides.get(i)).is_some_and(|g| g.vertical) => egui::CursorIcon::ResizeHorizontal,
Some(_) => egui::CursorIcon::ResizeVertical,
None => egui::CursorIcon::Default,
},
};
if resp.hovered() {
ui.ctx().set_cursor_icon(cursor);
@@ -1988,6 +1994,37 @@ mod tests {
assert!(super::grid_line_step(6.0, 0.0).is_none());
assert!(super::grid_line_step(6.0, 0.1).is_none(), "a 6 pt grid at 10% zoom is finer than one screen point");
}
/// #830: when the Format or Animation Painter is armed, the cursor shows the copy icon so the
/// user can see the tool is waiting for a click.
#[test]
fn format_painter_sets_the_cursor() {
let mut h = harness();
// Hover over the canvas so the pointer function runs and sets the cursor.
h.hover_at(egui::pos2(600.0, 400.0));
h.run_steps(2);
let before = h.output().platform_output.cursor_icon;
assert_ne!(before, egui::CursorIcon::Copy, "no painter is armed yet");
// Arming the format painter overrides the cursor to Copy (#830).
h.state_mut().session.painter = Some((Default::default(), false));
h.run_steps(3);
let after = h.output().platform_output.cursor_icon;
assert_eq!(after, egui::CursorIcon::Copy, "the format painter arms the Copy cursor");
}
/// #830: the Animation Painter arms the same cursor.
#[test]
fn animation_painter_sets_the_cursor() {
let mut h = harness();
h.hover_at(egui::pos2(600.0, 400.0));
h.run_steps(2);
let before = h.output().platform_output.cursor_icon;
assert_ne!(before, egui::CursorIcon::Copy, "no painter is armed yet");
h.state_mut().session.anim_painter = Some((vec![], false));
h.run_steps(3);
let after = h.output().platform_output.cursor_icon;
assert_eq!(after, egui::CursorIcon::Copy, "the animation painter arms the Copy cursor");
}
}
#[cfg(test)]