Guides: show the X or Y position beside the pointer while dragging (#7680)

This commit is contained in:
Hodd
2026-10-19 17:47:09 +02:00
parent d95fdf3181
commit 362bdf135e
3 changed files with 108 additions and 1 deletions
+3
View File
@@ -3913,6 +3913,9 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
if app.ui.view.shows(app.ui.view.show.canvas_guides) || crate::guide_layout_ui::preview(app, &doc).is_some() {
crate::rulers::draw_guides(app, &painter, &xf, &doc);
}
if app.session.active_index() == Some(idx) {
crate::rulers::draw_guide_drag_readout(app, &ctx);
}
// The pending crop frame shows on its own document only, not on the other tiles (#1918).
let foreign_crop = if app.session.active_index() == Some(idx) { None } else { app.ui.crop_rect.take() };
draw_drag_preview(app, &painter, &xf);
+102
View File
@@ -213,6 +213,30 @@ pub fn draw_guides(app: &PhotocraftApp, painter: &egui::Painter, xf: &ViewXform,
}
}
/// The position readout of a guide being dragged: `X:` for a vertical guide, `Y:` for a horizontal
/// one, measured from the ruler zero point in the ruler unit. Pixels keep one decimal, as
/// Photoshop's "X: 103.2 px".
pub fn guide_drag_readout(app: &PhotocraftApp, d: GuideDrag) -> Option<(&'static str, String)> {
let doc = &app.session.active()?.doc;
let ur = &app.session.prefs().units_and_rulers;
let axis = usize::from(!d.vertical);
let px = d.pos - doc.ruler_origin.get(axis).copied().unwrap_or(0.0);
let extent = if d.vertical { doc.size.width } else { doc.size.height } as f64;
let value = match ur.rulers {
Unit::Pixels => photocraft_engine::prefs::fmt_decimals(px, 1),
_ => ur.format(px, f64::from(doc.resolution_dpi), extent),
};
Some((if d.vertical { "X:" } else { "Y:" }, format!("{value} {}", ur.rulers.suffix())))
}
/// Draw [`guide_drag_readout`] beside the pointer while a guide is dragged.
pub fn draw_guide_drag_readout(app: &PhotocraftApp, ctx: &egui::Context) {
let (Some(d), Some(at)) = (app.guide_drag, ctx.pointer_latest_pos()) else { return };
if let Some((label, value)) = guide_drag_readout(app, d) {
crate::canvas::draw_value_readout(app, ctx, "guide-drag-readout", at, [label], [value]);
}
}
/// Existing guide under a document point (within 4 screen px).
pub fn guide_at(app: &PhotocraftApp, x: f64, y: f64) -> Option<(bool, usize)> {
// Hidden canvas guides are not interactive: the Move tool must reach the layer below.
@@ -737,6 +761,84 @@ mod tests {
assert!(h.query_by_label("17").is_some(), "horizontal guide: Y readout");
}
/// A dragged guide shows its position beside the pointer: X for a vertical guide, Y for a
/// horizontal one, from the zero point, in the ruler unit (pixels to one decimal).
#[test]
fn guide_drag_shows_its_position_beside_the_pointer() {
use egui_kittest::kittest::Queryable;
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
let v = GuideDrag { vertical: true, index: Some(0), pos: 103.24, press: 50.0 };
assert_eq!(guide_drag_readout(&app, v), Some(("X:", "103.2 px".to_owned())));
let h = GuideDrag { vertical: false, index: None, pos: 40.0, press: 40.0 };
assert_eq!(guide_drag_readout(&app, h), Some(("Y:", "40 px".to_owned())));
app.run("view.setRulerOrigin", json!({"x": 3.0, "y": 10.0})).unwrap();
assert_eq!(guide_drag_readout(&app, h), Some(("Y:", "30 px".to_owned())), "from the zero point");
app.run("view.resetRulerOrigin", json!({})).unwrap();
app.run("prefs.set", json!({"path": "unitsAndRulers.rulers", "value": "percent"})).unwrap();
assert_eq!(guide_drag_readout(&app, GuideDrag { vertical: true, index: None, pos: 103.0, press: 0.0 }).map(|r| r.0), Some("X:"));
app.run("prefs.set", json!({"path": "unitsAndRulers.rulers", "value": "pixels"})).unwrap();
app.guide_drag = Some(v);
let mut h = egui_kittest::Harness::builder().with_size(vec2(300.0, 200.0)).build_ui_state(|ui, app| draw_guide_drag_readout(app, ui.ctx()), app);
h.event(egui::Event::PointerMoved(pos2(50.0, 50.0)));
h.run_steps(2);
assert!(h.query_by_label("X:").is_some() && h.query_by_label("103.2 px").is_some());
h.state_mut().guide_drag = None;
h.run_steps(2);
assert!(h.query_by_label("X:").is_none(), "no readout once the drag ends");
}
/// Moving a horizontal guide with the Move tool on the real canvas: `Y:` follows the pointer.
#[test]
fn moving_a_guide_on_the_canvas_shows_its_y() {
use egui_kittest::kittest::Queryable;
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app.run("view.newGuide", json!({"orientation": "horizontal", "position": 100})).unwrap();
app.sync_views();
app.ui.extras.rulers = false;
app.ui.extras.snap = false;
app.ui.tool = crate::state::Tool::Move;
let mut h = egui_kittest::Harness::builder().with_size(vec2(800.0, 600.0)).build_ui_state(
|ui, app: &mut PhotocraftApp| {
if !ui.ctx().fonts(|f| f.families().contains(&egui::FontFamily::Name("medium".into()))) {
return;
}
egui::CentralPanel::default().show(ui, |ui| crate::canvas::document_area(app, ui));
},
app,
);
PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::ALL[0]);
h.run_steps(4);
let v = &mut h.state_mut().ui.views[0];
(v.zoom, v.center, v.fit_pending) = (1.0, [200.0, 150.0], false);
h.run_steps(2);
let at = |h: &egui_kittest::Harness<'static, PhotocraftApp>, x: f32, y: f32| {
let app = h.state();
let v = &app.ui.views[0];
let rect = content_rect(app, app.last_canvas_rect);
ViewXform { rect, zoom: v.zoom, center: v.center, flip: false, rotation: 0.0, aspect: 1.0 }.to_screen(x, y)
};
let press = at(&h, 200.0, 100.0);
h.event(egui::Event::PointerMoved(press));
h.run_steps(1);
h.event(egui::Event::PointerButton { pos: press, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE });
h.run_steps(1);
for y in [110.0, 125.0, 140.0] {
h.event(egui::Event::PointerMoved(at(&h, 210.0, y)));
h.run_steps(2);
}
assert!(h.state().guide_drag.is_some_and(|d| !d.vertical), "a horizontal guide is being dragged");
assert!(h.query_by_label("Y:").is_some() && h.query_by_label("140 px").is_some(), "Y readout at the guide");
assert!(h.query_by_label("X:").is_none());
let up = at(&h, 210.0, 140.0);
h.event(egui::Event::PointerButton { pos: up, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE });
h.run_steps(2);
assert_eq!(h.state().session.active().unwrap().doc.guides.horizontal, vec![140.0]);
assert!(h.query_by_label("Y:").is_none(), "gone after the drop");
}
#[test]
fn percentage_grid_spacing_uses_both_canvas_dimensions() {
let mut g = GuidesGridAndSlices { grid_unit: Unit::Percent, gridline_every: 20.0, subdivisions: 2, ..Default::default() };
+3 -1
View File
@@ -1,6 +1,6 @@
# UI parity: tools, handles, modifiers and feel
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Pen Auto Add/Delete on the path being drawn, #6140; Show Transform Controls frames every selected layer; guide keys and clicks; menu bar from the keyboard, #4988; Crop tool Content-Aware Fill; on-canvas corner radius widgets, TOOL-214-12; Curvature Pen edits the active path; Clip Thumbnails to Layer Bounds; Pen ⇧ 45°; last selected tool restored at launch; Layers panel filter-type dropdown with Kind, Name and Mode, #5048; Free Transform box and marching ants follow the selection, #5297; History: the trash deletes the selected snapshot, rows drag onto the trash, #5324; smart filter reordering by drag; smart object perspective row corrected to done, #426; Free Transform H/V skew fields; toolbar tool shortcuts can be reassigned; ⇧-dragged guides land on ruler ticks; layer and selection keys; tool keys; spring-loaded tool letters; type formatting keys; Pen Auto Add/Delete, #4144; smart filter rows; stale missing-tool rows corrected: Color Replacement, Red Eye and the Add/Delete Anchor Point pair are live toolbar tools, Frame landed; no percentage changed) · **Target:** Adobe Photoshop 2026
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (a dragged guide shows its X or Y beside the pointer, #7680; Pen Auto Add/Delete on the path being drawn, #6140; Show Transform Controls frames every selected layer; guide keys and clicks; menu bar from the keyboard, #4988; Crop tool Content-Aware Fill; on-canvas corner radius widgets, TOOL-214-12; Curvature Pen edits the active path; Clip Thumbnails to Layer Bounds; Pen ⇧ 45°; last selected tool restored at launch; Layers panel filter-type dropdown with Kind, Name and Mode, #5048; Free Transform box and marching ants follow the selection, #5297; History: the trash deletes the selected snapshot, rows drag onto the trash, #5324; smart filter reordering by drag; smart object perspective row corrected to done, #426; Free Transform H/V skew fields; toolbar tool shortcuts can be reassigned; ⇧-dragged guides land on ruler ticks; layer and selection keys; tool keys; spring-loaded tool letters; type formatting keys; Pen Auto Add/Delete, #4144; smart filter rows; stale missing-tool rows corrected: Color Replacement, Red Eye and the Add/Delete Anchor Point pair are live toolbar tools, Frame landed; no percentage changed) · **Target:** Adobe Photoshop 2026
How PhotoCraft's direct manipulation compares with Photoshop 2026 (27.11.0): the toolbox, the
handles and modifier keys of each gesture, nudging, snapping, numeric precision, navigation,
@@ -156,6 +156,7 @@ Triangle and Curvature Pen (minus legacy Rounded Rectangle and Targeted Adjustme
| Smart guide distance and spacing labels | missing | |
| Ruler units px/in/cm/mm/pt/pica/% | done | |
| ⇧-drag a guide out of a ruler: it lands on the nearest ruler tick | done | `rulers.rs` `snap_to_tick` (#3770) |
| A dragged guide shows its position beside the pointer (X for vertical, Y for horizontal, in the ruler unit) | done | `rulers.rs` `guide_drag_readout` (#7680) |
| Status bar follows the ruler unit | partial | #2602 |
| Ruler corner and gaps drawn as Photoshop | partial | #2600, #2601, #2551 |
| Guides stored as f64 | missing | `Vec<f32>` (UI-217-20) |
@@ -227,6 +228,7 @@ Paragraph; the rest open as in-app floating windows.
| Date | Change | Summary |
|---|---|---|
| 2026-10-19 | minor | A guide being moved or dragged out of a ruler shows an `X:` / `Y:` position readout beside the pointer, from the zero point in the ruler unit (#7680) |
| 2026-10-13 | minor | Menu bar from the keyboard: Alt / F10 focus, Alt+letter, underlined mnemonics (#4988) |
| 2026-10-13 | minor | Layers panel filter row: Kind is a real dropdown with Kind, Name and Mode filters, drivable by `ui.set {layerFilter}` (#5048); row added as partial |
| 2026-10-13 | minor | Stale "missing tool" rows corrected against main: Color Replacement (`paint.colorReplacement`), Red Eye (`paint.redEye`) and Add/Delete Anchor Point (the P flyout cycle) are live toolbar tools, so only Art History Brush is missing; no percentage or estimate changed |