fix: PDF finger ink, chrome UX, OneNote pens, and pen physics
Some checks failed
CI / Windows build (push) Has been cancelled

Wire finger drawing on PDF without breaking pinch; auto-hide page scrubber and fix bounce; share sticky tools with resize and per-page remember; side-button select; separate pen slots with colors; rnote pressure shapes plus tip-velocity width and lower stroke latency.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-06 16:01:01 +08:00
parent 85af037b7d
commit f31dd0fb52
17 changed files with 835 additions and 234 deletions

View File

@@ -22,6 +22,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import '../engine/brush.dart';
import '../engine/pen_physics.dart';
import '../engine/stroke_eraser.dart';
import '../engine/stroke_geometry.dart' show kDefaultPenThinning;
import '../engine/stroke_model.dart';
@@ -33,6 +34,7 @@ import '../input/pressure_curve.dart';
import '../input/pen_input_service.dart';
import '../../diagnostics/pen_event_ring.dart';
import '../engine/shape_geometry.dart';
import 'dart:math' as math;
import '../render/ink_picture_cache.dart';
import '../render/live_ink_painter.dart' as render;
import '../render/static_ink_painter.dart' as render;
@@ -230,6 +232,10 @@ class _PenCanvasState extends State<PenCanvas> {
/// undo snapshot is recorded once, on the first drag delta — see _extendStroke).
bool _selectDragging = false;
/// Tip-velocity tracker for [tipVelocityWidthScale] (physical ink starvation).
Offset? _lastTipNorm;
Duration? _lastTipTime;
/// True when the active stylus reports the eraser signal (barrel button or
/// inverted stylus), detected on hover/down.
bool _eraserActive = false;
@@ -399,7 +405,8 @@ class _PenCanvasState extends State<PenCanvas> {
if (action == _lastHwAction) return;
_lastHwAction = action;
if (action == PenButtonAction.undo ||
action == PenButtonAction.toggleTool) {
action == PenButtonAction.toggleTool ||
action == PenButtonAction.select) {
widget.onPenButtonAction?.call(action);
}
}
@@ -441,7 +448,7 @@ class _PenCanvasState extends State<PenCanvas> {
/// Map a global pointer position into normalized page coords using the
/// shared transform (inverse) and this widget's geometry.
PenPoint? _toNormalized(Offset globalPosition, double? pressure,
{double? tilt}) {
{double? tilt, Duration? timeStamp}) {
final box = context.findRenderObject() as RenderBox?;
if (box == null) return null;
final local = box.globalToLocal(globalPosition);
@@ -451,7 +458,23 @@ class _PenCanvasState extends State<PenCanvas> {
final nx = scene.dx / widget.pageSize.width;
final ny = scene.dy / widget.pageSize.height;
return PenPoint(nx, ny, pressure, tilt: tilt);
double? shaped = pressure;
if (shaped != null && timeStamp != null && _lastTipNorm != null &&
_lastTipTime != null) {
final dt = (timeStamp - _lastTipTime!).inMicroseconds / 1e6;
if (dt > 0) {
final dx = nx - _lastTipNorm!.dx;
final dy = ny - _lastTipNorm!.dy;
final speed = math.sqrt(dx * dx + dy * dy) / dt;
shaped = (shaped * tipVelocityWidthScale(_currentBrush, speed))
.clamp(0.0, 1.0);
}
}
_lastTipNorm = Offset(nx, ny);
_lastTipTime = timeStamp;
return PenPoint(nx, ny, shaped, tilt: tilt);
}
// --- Stroke lifecycle -----------------------------------------------------
@@ -464,8 +487,10 @@ class _PenCanvasState extends State<PenCanvas> {
_shapeStart = null;
_selectLast = null;
_selectDragging = false;
_lastTipNorm = null;
_lastTipTime = null;
final p = _toNormalized(event.position, _normalizedPressure(event),
tilt: _tiltFor(event));
tilt: _tiltFor(event), timeStamp: event.timeStamp);
if (_eraserActive || widget.tool == CanvasTool.eraser) {
_eraserCursor.value = p;
@@ -502,7 +527,7 @@ class _PenCanvasState extends State<PenCanvas> {
void _extendStroke(PointerMoveEvent event) {
final p = _toNormalized(event.position, _normalizedPressure(event),
tilt: _tiltFor(event));
tilt: _tiltFor(event), timeStamp: event.timeStamp);
if (p == null) return;
if (_eraserActive || widget.tool == CanvasTool.eraser) {