diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs index 97b787ef3..9a953c36e 100644 --- a/crates/ui-egui/src/grid.rs +++ b/crates/ui-egui/src/grid.rs @@ -2258,10 +2258,22 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: & // Release. if resp.drag_stopped() || (!pointer.any_down() && app.grid.drag != Drag::None && !resp.dragged()) { match std::mem::take(&mut app.grid.drag) { - Drag::Select if resp.drag_stopped() => { - if let Some(p) = pos { + Drag::Select => { + if resp.drag_stopped() + && let Some(p) = pos + { select_to_pointer(app, geo, sh, wb, p); } + // An armed Format Painter pastes onto the cells just clicked or dragged over + // (not while pointing a formula reference, which also drags a Select). + if app.session.format_painter.is_some() && !app.editor.as_ref().is_some_and(|ed| ed.can_point()) { + let dest = app.session.active().map(|d| d.selection.current().a1()); + if let Some(dest) = dest + && let Err(e) = app.run("edit.formatPainter", json!({"apply": dest})) + { + app.message = Some(("GridCraft".into(), crate::clean_error(&e))); + } + } } Drag::Fill { target: Some(t) } => { let src = sel.current(); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 9c6d89eeb..5fd93c117 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -569,6 +569,7 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { } let fp_on = app.session.format_painter.is_some(); let fp = toggle_button(ui, Icon::Brush, fp_on, "Format Painter (double-click to keep it on)"); + fp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), fp_on, "Format Painter")); control_keytip(app, ui, &fp, "edit.formatPainter"); if fp.double_clicked() { act(app, "edit.formatPainter", json!({"sticky": true})); diff --git a/crates/ui-egui/tests/format_painter.rs b/crates/ui-egui/tests/format_painter.rs new file mode 100644 index 000000000..1a9c5742c --- /dev/null +++ b/crates/ui-egui/tests/format_painter.rs @@ -0,0 +1,78 @@ +//! Format Painter through the real ribbon button and a click on the grid. + +use egui::accesskit::Toggled; +use egui::{Event, Modifiers, PointerButton, Pos2}; +use egui_kittest::kittest::{NodeT, Queryable}; +use gridcraft_engine::Session; +use gridcraft_engine::core::CellRef; +use gridcraft_engine::model::Style; +use gridcraft_ui_egui::{SheetApp, grid::Geo}; +use serde_json::json; + +type Harness = egui_kittest::Harness<'static, SheetApp>; + +fn harness() -> Harness { + let mut session = Session::new(); + session.new_workbook(); + session.execute("range.setValues", json!({"range": "A1", "values": [["source"]]})).unwrap(); + session.execute("home.bold", json!({"range": "A1", "on": true})).unwrap(); + session.execute("home.fillColor", json!({"range": "A1", "color": "#FFFF00"})).unwrap(); + session.execute("selection.set", json!({"cell": "A1"})).unwrap(); + let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SheetApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + SheetApp::new(session, Default::default()), + ); + h.run_steps(4); + h +} + +fn style(h: &Harness, a1: &str) -> Style { + let doc = h.state().session.active().unwrap(); + let sheet = doc.wb.active().unwrap(); + doc.wb.styles.get(sheet.style_id(CellRef::parse(a1).unwrap())).clone() +} + +fn point(h: &Harness, a1: &str) -> Pos2 { + let app = h.state(); + let sheet = app.session.active().unwrap().wb.active().unwrap(); + Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll).cell_rect(sheet, CellRef::parse(a1).unwrap()).center() +} + +fn pressed(h: &Harness) -> bool { + h.get_by_label("Format Painter").accesskit_node().toggled() == Some(Toggled::True) +} + +fn click_cell(h: &mut Harness, a1: &str) { + let at = point(h, a1); + for event in [ + Event::PointerMoved(at), + Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }, + Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }, + ] { + h.input_mut().events.push(event); + h.run_steps(1); + } + h.run_steps(2); +} + +#[test] +fn ribbon_button_picks_up_the_format_and_a_click_on_a_cell_pastes_it() { + let mut h = harness(); + let source = style(&h, "A1"); + assert!(source.font.bold); + assert_ne!(style(&h, "C3"), source); + + h.get_by_label("Format Painter").click(); + h.run_steps(2); + assert!(h.state().session.format_painter.is_some(), "the button must arm the painter"); + assert!(pressed(&h), "the armed button must show as pressed"); + + click_cell(&mut h, "C3"); + assert_eq!(style(&h, "C3"), source, "clicking C3 must paste A1's format"); + assert!(h.state().session.format_painter.is_none(), "a single-click painter disarms after one use"); + assert!(!pressed(&h)); +}