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

When the Format Painter or Animation Painter was armed, the mouse cursor
stayed as the default arrow. The user had no visual indication that the
tool was active and waiting for a click on a shape to apply the copied
format.

The canvas now sets the Copy cursor while either painter is armed, and
goes back to its usual cursor once the tool is done. The override takes
precedence over the other cursor assignments (resize, move, crosshair,
text), because the painter's whole purpose at that moment is to receive
the next click.

The kittest covers both painters: it hovers over the canvas, checks the
cursor is not the Copy one yet, arms the painter and checks it is. With
the painter override removed both fail - the cursor stays Move instead
of Copy.

ROADMAP progress-log row added.
This commit is contained in:
Sazid Al Bayazid
2026-10-20 01:04:45 +06:00
parent f34f4121c8
commit 756ca23d31
2 changed files with 64 additions and 26 deletions
+1
View File
@@ -144,6 +144,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. |
+63 -26
View File
@@ -753,34 +753,40 @@ 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 { handle, .. })) => match handle {
0 | 4 => egui::CursorIcon::ResizeNwSe,
2 | 6 => egui::CursorIcon::ResizeNeSw,
1 | 5 => egui::CursorIcon::ResizeVertical,
_ => egui::CursorIcon::ResizeHorizontal,
},
(_, 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 { handle, .. })) => match handle {
0 | 4 => egui::CursorIcon::ResizeNwSe,
2 | 6 => egui::CursorIcon::ResizeNeSw,
1 | 5 => egui::CursorIcon::ResizeVertical,
_ => egui::CursorIcon::ResizeHorizontal,
},
(_, 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);
@@ -1594,6 +1600,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)]