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

@@ -1,10 +1,11 @@
// lib/editor/canvas/sticky_note_overlay.dart
//
// Paper-sticky UX for PDF scratch links: a floating card on the viewer that
// inks into the SAME SidecarRepository the PDF editor holds (no second open,
// no split-view race). Collapsed = page marker; expanded = this overlay.
// inks into the SAME SidecarRepository the PDF editor holds. Shares the parent
// editor's brush/color/tool so there is one toolbar mental model (OneNote-like).
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
@@ -16,8 +17,8 @@ import '../engine/brush.dart';
import '../input/pen_config.dart' show kDefaultEraserRadius;
import '../notebook/ink_stroke_adapter.dart';
import '../persistence/sidecar_repository.dart';
import 'editor_tool.dart';
import 'pen_canvas.dart';
import 'pen_palette_widgets.dart';
import 'pen_stroke.dart';
/// Default world size for a fresh sticky scratchpad (absolute px).
@@ -31,6 +32,10 @@ class StickyNoteOverlay extends StatefulWidget {
required this.repo,
required this.onClose,
required this.onDelete,
this.brush = BrushKind.ballpoint,
this.color = const Color(0xFF1A1A1A),
this.tool = EditorToolKind.brush,
this.allowFingerDrawing = false,
});
final ScratchLink link;
@@ -38,6 +43,12 @@ class StickyNoteOverlay extends StatefulWidget {
final VoidCallback onClose;
final VoidCallback onDelete;
/// Shared from the parent PDF toolbar (no mini duplicate palette).
final BrushKind brush;
final Color color;
final EditorToolKind tool;
final bool allowFingerDrawing;
@override
State<StickyNoteOverlay> createState() => _StickyNoteOverlayState();
}
@@ -48,12 +59,13 @@ class _StickyNoteOverlayState extends State<StickyNoteOverlay> {
final TransformationController _transform = TransformationController();
List<InkStroke> _strokes = [];
Size _world = kStickyWorldSize;
CanvasTool _tool = CanvasTool.pen;
BrushKind _brush = BrushKind.ballpoint; // ignore: prefer_final_fields — reserved for brush picker
Color _color = kInkPalette.first;
Timer? _saveTimer;
bool _dirty = false;
/// On-screen card size (user-resizable). World canvas stays [_world].
double _cardW = 300;
double _cardH = 360;
@override
void initState() {
super.initState();
@@ -61,13 +73,16 @@ class _StickyNoteOverlayState extends State<StickyNoteOverlay> {
if (pad != null) {
_world = Size(pad.canvasWidth, pad.canvasHeight);
_strokes = pad.strokes.where((s) => isFreehandTool(s.tool)).toList();
// Prefer a card that roughly matches aspect of the world, clamped.
final aspect = _world.width / math.max(_world.height, 1);
_cardW = (280.0 * aspect).clamp(220.0, 520.0);
_cardH = (_cardW / aspect + 40).clamp(260.0, 640.0);
}
}
@override
void dispose() {
_saveTimer?.cancel();
// Best-effort sync save before leaving the overlay.
if (_dirty) {
widget.repo.scheduleScratchpadSave(
widget.link.id,
@@ -134,6 +149,32 @@ class _StickyNoteOverlayState extends State<StickyNoteOverlay> {
widget.onClose();
}
CanvasTool get _canvasTool {
switch (widget.tool) {
case EditorToolKind.eraser:
return CanvasTool.eraser;
case EditorToolKind.select:
return CanvasTool.select;
case EditorToolKind.highlighter:
case EditorToolKind.brush:
case EditorToolKind.shape:
case EditorToolKind.text:
return CanvasTool.pen;
}
}
BrushKind get _canvasBrush =>
widget.tool == EditorToolKind.highlighter
? BrushKind.highlighter
: widget.brush;
void _onResizeDrag(DragUpdateDetails d) {
setState(() {
_cardW = (_cardW + d.delta.dx).clamp(220.0, 640.0);
_cardH = (_cardH + d.delta.dy).clamp(240.0, 720.0);
});
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
@@ -142,117 +183,96 @@ class _StickyNoteOverlayState extends State<StickyNoteOverlay> {
borderRadius: BorderRadius.circular(4),
color: const Color(0xFFFFF8E1),
child: SizedBox(
width: 280,
height: 340,
child: Column(
width: _cardW,
height: _cardH,
child: Stack(
children: [
Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: 4),
decoration: const BoxDecoration(
color: Color(0xFFFFE082),
borderRadius: BorderRadius.vertical(top: Radius.circular(4)),
),
child: Row(
children: [
const Icon(Icons.sticky_note_2, size: 18),
const SizedBox(width: 6),
const Expanded(
child: Text(
'便利贴',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
Column(
children: [
Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: 4),
decoration: const BoxDecoration(
color: Color(0xFFFFE082),
borderRadius: BorderRadius.vertical(top: Radius.circular(4)),
),
IconButton(
tooltip: '删除',
icon: const Icon(Icons.delete_outline, size: 18),
visualDensity: VisualDensity.compact,
onPressed: () async {
await _saveNow();
widget.onDelete();
},
),
IconButton(
tooltip: '收起',
icon: const Icon(Icons.close, size: 18),
visualDensity: VisualDensity.compact,
onPressed: _close,
),
],
),
),
SizedBox(
height: 32,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 6),
children: [
for (final c in kInkPalette)
GestureDetector(
onTap: () => setState(() => _color = c),
child: Container(
width: 18,
height: 18,
margin: const EdgeInsets.symmetric(
horizontal: 3, vertical: 7),
decoration: BoxDecoration(
color: c,
shape: BoxShape.circle,
border: Border.all(
color: _color == c ? cs.primary : cs.outlineVariant,
width: _color == c ? 2 : 1,
child: Row(
children: [
const Icon(Icons.sticky_note_2, size: 18),
const SizedBox(width: 6),
const Expanded(
child: Text(
'便利贴',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
Expanded(
child: ClipRect(
child: PenCanvas(
pageSize: _world,
strokes: penStrokesFromInk(_strokes, _world),
transformationController: _transform,
tool: _tool,
brush: _brush,
color: _color,
strokeWidth: 0.008,
eraserRadius: kDefaultEraserRadius,
minScale: 0.2,
maxScale: 4.0,
onStrokeComplete: _onStrokeComplete,
onEraseStroke: _onErase,
pageWidget: const ColoredBox(color: Color(0xFFFFFDE7)),
Text(
'用顶栏笔/色',
style: TextStyle(
fontSize: 11,
color: cs.onSurface.withValues(alpha: 0.55),
),
),
IconButton(
tooltip: '删除',
icon: const Icon(Icons.delete_outline, size: 18),
visualDensity: VisualDensity.compact,
onPressed: () async {
await _saveNow();
widget.onDelete();
},
),
IconButton(
tooltip: '收起',
icon: const Icon(Icons.close, size: 18),
visualDensity: VisualDensity.compact,
onPressed: _close,
),
],
),
),
),
Expanded(
child: ClipRect(
child: PenCanvas(
pageSize: _world,
strokes: penStrokesFromInk(_strokes, _world),
transformationController: _transform,
tool: _canvasTool,
brush: _canvasBrush,
color: widget.color,
strokeWidth: brushProfileFor(_canvasBrush).baseWidthFraction,
eraserRadius: kDefaultEraserRadius,
allowFingerDrawing: widget.allowFingerDrawing,
minScale: 0.2,
maxScale: 4.0,
onStrokeComplete: _onStrokeComplete,
onEraseStroke: _onErase,
pageWidget: const ColoredBox(color: Color(0xFFFFFDE7)),
),
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
child: Row(
children: [
IconButton(
tooltip: '',
icon: Icon(
Icons.edit,
size: 18,
color: _tool == CanvasTool.pen ? cs.primary : null,
Positioned(
right: 0,
bottom: 0,
child: GestureDetector(
onPanUpdate: _onResizeDrag,
child: MouseRegion(
cursor: SystemMouseCursors.resizeUpLeftDownRight,
child: SizedBox(
width: 28,
height: 28,
child: Icon(
Icons.south_east,
size: 16,
color: cs.onSurface.withValues(alpha: 0.45),
),
onPressed: () => setState(() => _tool = CanvasTool.pen),
),
IconButton(
tooltip: '橡皮',
icon: Icon(
Icons.cleaning_services_outlined,
size: 18,
color: _tool == CanvasTool.eraser ? cs.primary : null,
),
onPressed: () => setState(() => _tool = CanvasTool.eraser),
),
],
),
),
),
],