Skip to content

feat: ruler tool - #1796

Open
andrinwinzap wants to merge 11 commits into
flxzt:mainfrom
andrinwinzap:feat/ruler-tool
Open

feat: ruler tool#1796
andrinwinzap wants to merge 11 commits into
flxzt:mainfrom
andrinwinzap:feat/ruler-tool

Conversation

@andrinwinzap

Copy link
Copy Markdown

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

  • Draw against the edge – starting a brush stroke within the snap zone of a ruler edge locks the stroke to that straight edge for its whole duration. The snap decision is made once at stroke start, so a stroke either follows the ruler or doesn't — it never jumps mid-stroke.
  • Reposition – dragging the ruler body (pen / one finger / mouse) moves it; the ruler lives in window-relative coordinates, so panning or zooming the canvas leaves it fixed on screen.
  • Rotate – a two-finger gesture rotates around the projected finger centroid; mouse-wheel / trackpad scrolling over the body also rotates it, with speed-adaptive scaling and a short session so the pivot stays put across successive scrolls.
  • Angle snapping – optional snap to 0° / ±45° / ±90° with hysteresis (easy to enter a snap, a small nudge to leave it).
  • Tick marks & angle dial – three-tier tick marks along the edges and an optional dial showing the current angle at the center of rotation.
  • Configuration popover (in the brush page) exposes: snap distance, angle-snap toggle, show-dial toggle, body width, tick spacing, body opacity, and scroll rotation step. Persisted settings survive across sessions; live ruler position/angle/visibility are session-only.

Implementation notes

  • New 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.
  • Rendering lives in crates/rnote-engine/src/pens/ruler.rs.
  • The brush pen (crates/rnote-engine/src/pens/brush.rs) gains a DraggingRuler state and the per-stroke snap logic.
  • UI wiring (toggle, popover, symbolic icon) is in rnote-ui (penssidebar/brushpage.rs + .ui), and the gesture handling (drag / two-finger rotate / scroll-rotate) is in crates/rnote-ui/src/canvaswrapper.rs.

Checks

  • cargo fmt --check – clean
  • ui-cargo-clippy / cli-cargo-clippy – no warnings
  • cargo-test (nextest) – all passing

Screencast

Screencast_20260706_214039.webm

AI disclosure

This contribution was developed with the assistance of LLM/GenAI tools.

andrinwinzap and others added 11 commits June 3, 2026 22:58
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.
@andrinwinzap andrinwinzap changed the title Add a OneNote-style ruler tool feat: ruler tool Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant