feat: ruler tool - #1796
Open
andrinwinzap wants to merge 11 commits into
Open
Conversation
A draggable, rotatable straight-edge for the brush pen. Brush strokes within a configurable snap zone of the ruler's long edge get projected onto that edge, so users can draw straight lines along the ruler. User-facing behavior: - Toggle in the brush sidebar shows/hides the ruler; the band stays fixed relative to the window (not the document) as the canvas is panned or zoomed. - One-finger / mouse / stylus drag on the body translates the ruler. - Two-finger gesture rotates around the projected centroid and can pan via centroid drift. - Scroll wheel / trackpad over the body rotates around the pointer. - Strokes that start within the snap zone "lock" to the ruler edge for the rest of the stroke; strokes that start outside never snap. - Angle dial (with world-aligned tick markings and red direction indicators) sits at the rotation pivot and shows the current angle. - Optional snap-to-common-angles (0/+-45/+-90) during rotation. - Ruler colors flip automatically based on the page background's luminance so the ruler is visible on light and dark backgrounds. Settings exposed in the brush sidebar popover: - Show ruler toggle - Snap distance (% of ruler width) - Snap to common angles toggle - Show angle dial toggle - Ruler width, tick spacing, body opacity - Scroll rotation sensitivity Implementation notes: - New RulerConfig under brush_config holds all persisted ruler state; position/dial/angle are session-only (#[serde(skip)]). - New pens/ruler.rs draws the band, ticks and dial on top of the active pen via PenHolder::draw_on_doc, so the ruler shows regardless of which pen is selected. - Position is stored in scroller (window-relative) pixel coordinates and converted to doc coords at render/snap time via the camera transform. - Brush.rs gains a DraggingRuler state and a sticky ruler_snap_side on BrushState::Drawing so the snap decision is taken once per stroke. - Two-finger handling lives in CanvasWrapper alongside the existing zoom gesture; scroll-rotate uses a short-lived "session" so the pivot is locked for the duration of a gesture rather than being recomputed per event.
- Reorder ruler settings: angle snap, show dial, width, opacity, tick
spacing, snap distance, scroll sensitivity.
- Strip unit suffixes from titles ("Body Opacity (%)" -> "Body Opacity",
etc.) and ensure the descriptions cover the units instead.
- Drop angle snap from scroll rotation; keep it only for the two-finger
gesture. Snap on a discrete wheel device was hard to escape: a single
click stayed within the snap threshold and instantly re-snapped.
- Add a speed-adaptive multiplier for mouse-wheel scroll: precise when
the user rolls slowly, faster when they roll quickly. Multiplier is
clamp(200ms / dt, 0.4, 3.0). Trackpad ('Surface') events are left
as-is because they already encode speed in |dy|.
- Add a dead zone to the scroll-rotation session: when the session
times out, if the pointer is still within one body_half_width of the
previous pivot, revive that pivot instead of recomputing. Stops the
dial from drifting along the ruler when the user scrolls slowly and
the inter-event gap exceeds the session timeout.
- Update the 'Snap to Common Angles' subtitle to clarify the toggle
applies to two-finger rotation only.
- Add hysteresis to the two-finger snap (narrow 'leave' threshold once
locked on a target) so finger jitter doesn't keep the ruler stuck.
…display - Switch scroll controller to Capture phase so the canvas's Scrollable machinery can't consume a scroll mid-rotation and cause the rotation to slip into a canvas pan. - Add a longer revival window (3s): if pointer_pos is momentarily None but a recent session exists, keep rotating around the locked pivot instead of returning false and letting the scroll propagate. - Prime pointer_pos on the motion controller's `enter` signal too, so it's set the instant the pointer enters the widget rather than only on the next motion event. - Two-finger snap hysteresis: apply the narrow leave threshold only to the target the ruler is currently locked on; other targets keep the normal 3° enter threshold. Previously, locking on (say) 90° narrowed the window for every target, so gestures glided past 0° and ±45°. - Display: a rounded -90° now shows as 90° (same physical orientation, and the displayable range is (-90°, 90°]).
Brings in upstream changes through f3f7efd, including: - Real-size zoom button (flxzt#1735) - CI / cargo-deny / licenses - nalgebra -> glam math migration Ported the ruler additions to the new math API: - na::Vector2<f64> -> p2d::math::Vector2 - na::vector![x, y] -> Vector2::new(x, y) - na::vector![0.0, 0.0] -> Vector2::ZERO - v.dot(&w) -> v.dot(w) - v.norm() -> v.length() - Aabb::new(na::point!{...}, ...) -> Aabb::new(Vector2::new(...), ...) - extents().norm() -> extents().length() Also dropped the unused AabbExt import in ruler.rs.
- Validate the cached scroll-session pivot against the current ruler centerline before reusing it. If another operation (e.g. a two-finger rotation) moved/rotated the ruler in between, the cached pivot may be off the line; using it would rotate around an off-line point and put the dial outside the band. Filter expired in that case. - Apply the same dead zone on the very first scroll: if the cursor is within 1.5x body_half_width of the current dial_pos (and the dial is on the line), reuse the dial position as the pivot instead of jumping it to a freshly-projected cursor spot. - Only enter touch-style two-finger ruler manipulation when the event source is an actual touchscreen. Trackpad pinch/rotate gestures also fire GestureZoom/GestureRotate; they should fall through to canvas zoom rather than putting the user into the touch-only ruler mode. - Consume horizontal scroll while rotating the ruler. The scroll controller now listens on both axes so a trackpad scroll with a sideways component doesn't bleed into a canvas pan; the handler returns Propagation::Stop on success, consuming the whole event.
Interpolate the ruler body color against `body_opacity` so the body stays visible at low opacity (contrasting the canvas) and gives enough contrast to the tick/text markings at high opacity.
When touch-drawing is off, the one-finger ruler drag goes through canvas_drag_gesture. Without intervention the ScrolledWindow's kinetic scroll steals the touch sequence once the finger crosses its drag threshold, so the ruler "moves a bit and then the canvas pans". Disable kinetic scrolling for the duration of a ruler drag (restored on drag_end). The touch sequence is intentionally left unclaimed so canvas_zoom_gesture can still grab a second finger for rotation.
Two-finger ruler manipulation was using the touch centroid for its hit-test, so fingers placed on opposite sides of the ruler with neither on the body would still pass (the midpoint lands on the centerline). Check each touch position individually instead; every active touch must lie inside the body strip for the gesture to enter ruler mode.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add a OneNote-style ruler tool
Summary
This adds an on-canvas ruler (straight-edge) to the brush tool, the first of the helper tools requested in #147. When enabled, a translucent ruler spans the viewport; brush strokes drawn along its edge are snapped to a perfectly straight line, and the ruler can be freely repositioned and rotated with pen, touch, or mouse.
Addresses #147 (ruler portion; the protractor is left for a follow-up).
Motivation
Rnote had no way to draw a clean straight line freehand. A ruler is the most-requested helper tool in #147 and mirrors a familiar workflow from OneNote, making technical and mathematical sketches practical directly on the canvas.
What's included
Implementation notes
RulerConfig(crates/rnote-engine/src/pens/pensconfig/rulerconfig.rs) holds both the persisted preferences and the in-session runtime state, plus the coordinate conversions and snap math.crates/rnote-engine/src/pens/ruler.rs.crates/rnote-engine/src/pens/brush.rs) gains aDraggingRulerstate and the per-stroke snap logic.rnote-ui(penssidebar/brushpage.rs+.ui), and the gesture handling (drag / two-finger rotate / scroll-rotate) is incrates/rnote-ui/src/canvaswrapper.rs.Checks
cargo fmt --check– cleanui-cargo-clippy/cli-cargo-clippy– no warningscargo-test(nextest) – all passingScreencast
Screencast_20260706_214039.webm
AI disclosure
This contribution was developed with the assistance of LLM/GenAI tools.