From 756ca23d313f915e69069967fb579eb086a5213e Mon Sep 17 00:00:00 2001 From: Sazid Al Bayazid Date: Tue, 20 Oct 2026 01:04:43 +0600 Subject: [PATCH] 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. --- ROADMAP.md | 1 + crates/ui-egui/src/canvas.rs | 89 +++++++++++++++++++++++++----------- 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 93bc931e..2ca97a60 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. | diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index ba66aad8..127a991e 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -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)]