Browse / github.com/storytold/wordcraft / patch
Format Painter: double-click a word to paint it (#4410)
Feature Request: Allow double click word selection when applying Format Painter Hello! ### Problem Currently, applying formatting with the Format Painter requires dragging the cursor over the target text. It would be nice if double clicking a word while the painter is active selected and formatted that whole word. ### Current Behavior 1. Select text to copy formatting from. 2. Click the Format Painter tool. 3. Drag across target text to apply formatting. (Double-clicking a word fails to trigger the format application). ### Expected Behavior 1. Select text to copy formatting from. 2. Click the Format Painter tool. 3. Double click a target word. 4. The format applies to
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- MichaelMuClaude (imported from pull request #4502, open)
- Base
d522bfd8b19168aabcd9a64d588397c519c393f9- Size
- +55 −3 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
37fd704f781917cabefaf710
How it went
No other operator has reported using it yet.
Diff
raw · crates/ui-egui/src/canvas/input.rs crates/ui-egui/src/canvas/tests.rs
@@ -82,6 +82,14 @@ pub(super) fn clamped_scroll(before: egui::Vec2, delta: egui::Vec2, max: egui::V (before - delta).clamp(egui::Vec2::ZERO, max) } +/// Paint the selection with the Format Painter's formatting, when it is armed and something is+/// selected.+fn apply_format_painter(app: &mut WordApp) {+ if app.session.painter.is_some() && !app.session.sel.is_collapsed() {+ let _ = app.run("edit.pasteFormat", json!({}));+ }+}+ /// Routes pointer input: pen or eraser ink, object and crop drags, table border, selection and move /// handles, then page hit testing, outlines, headers and footers, equations, selection and caret /// interaction.@@ -152,6 +160,9 @@ pub(super) fn mouse(app: &mut WordApp, ui: &Ui, resp: &egui::Response, rects: &[ if released_clicks > 0 && app.canvas.unit_drag.as_ref().is_some_and(|d| d.clicks == released_clicks) { app.canvas.unit_drag = None; app.canvas.dragging = false;+ // Double-clicking a word (or triple-clicking a paragraph) with the Format Painter armed+ // paints it, as dragging across it does (#4410).+ apply_format_painter(app); return; } // Double-click in the header/footer area edits it (creating it when the page has none, #960);@@ -403,9 +414,7 @@ pub(super) fn mouse(app: &mut WordApp, ui: &Ui, resp: &egui::Response, rects: &[ app.canvas.dragging = false; app.canvas.column_drag = None; app.canvas.unit_drag = None;- if app.session.painter.is_some() && !app.session.sel.is_collapsed() {- let _ = app.run("edit.pasteFormat", json!({}));- }+ apply_format_painter(app); } // Hover a link: show its target, unless the context menu is opening or open: a tooltip draws // above the menu and would cover its first items (#3368).@@ -1053,6 +1053,49 @@ fn an_armed_format_painter_changes_the_pointer() { assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::Default, "the selection bar keeps its arrow"); } +/// #4410: with the Format Painter armed, a double-click on a word paints that word, and the+/// painter goes off again (it was only dragging across the text that painted it).+#[test]+fn double_clicking_a_word_with_the_format_painter_paints_it() {+ use egui::{Event, PointerButton};+ let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default());+ a.run("text.insert", json!({"text": "Bold source then target words"})).unwrap();+ a.run("view.commentsPane", json!({"value": false})).unwrap();+ a.run("select.text", json!({"text": "Bold"})).unwrap();+ a.run("format.bold", json!({})).unwrap();+ a.run("edit.formatPainter", json!({})).unwrap();+ let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).with_step_dt(1.0 / 60.0).build_ui_state(+ |ui, app: &mut WordApp| {+ let ctx = ui.ctx().clone();+ app.logic(&ctx);+ app.ui(ui);+ },+ a,+ );+ for _ in 0..6 {+ h.step();+ }+ assert!(h.state().session.painter.is_some(), "armed");+ let word_off = "Bold source then ".len() + 2;+ let c = h.state_mut().session.layout().caret(&Pos::body(0, word_off)).expect("caret");+ let at = page_to_screen(h.state_mut(), c.page, c.x, c.top + c.height / 2.0).expect("on screen");+ h.event(Event::PointerMoved(at));+ for _ in 0..2 {+ h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: true, modifiers: Default::default() });+ h.step();+ h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: Default::default() });+ h.step();+ }+ for _ in 0..3 {+ h.step();+ }+ let s = &h.state().session;+ let para = s.doc.para_at(&Pos::body(0, 0)).expect("paragraph");+ assert_eq!(para.props_of_char("Bold source then t".len()).bold, Some(true), "the double-clicked word is painted");+ assert_ne!(para.props_of_char("Bold source ".len()).bold, Some(true), "the other words are not");+ assert!(s.painter.is_none(), "the painter is used up");+}+ /// #1085: a selected balloon's buttons work (pressing them is not a click on the page that /// leaves the balloon): a new comment with no text resolves and deletes from its balloon. #[test]