collective

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

diff --git a/crates/ui-egui/src/canvas/input.rs b/crates/ui-egui/src/canvas/input.rs--- a/crates/ui-egui/src/canvas/input.rs+++ b/crates/ui-egui/src/canvas/input.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).diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs--- a/crates/ui-egui/src/canvas/tests.rs+++ b/crates/ui-egui/src/canvas/tests.rs@@ -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]