feat(note): rebuild note editor on the pen-first canvas
Some checks failed
CI / Windows build (push) Has been cancelled

Notes now use the single performant inking engine (PenCanvas) instead of
the old ink_canvas, per "all note features on the pen-first canvas".

- ink_stroke_adapter: pure InkStroke<->PenStroke bridge (normalize against
  a logical note page; drop non-freehand shapes/text). Round-trip tested.
- pen_palette_widgets: shared M3 ToolButton/PaletteDivider/RoundIconButton
  so PDF + note editors use identical chrome (PenEditorScreen migrated to
  them; its private copies deleted).
- PenNoteScreen: PenCanvas over a white logical page, undo/redo, title,
  save -> Note.strokes (+ local OCR for search). Pressure curve, eraser
  size/mode and palm rejection all inherited from the shared canvas.
- Route home (new/open) + search note hits -> PenNoteScreen; remove the
  now-redundant "Pen Canvas (beta)" spike button; delete the dead old
  note_editor_screen.

Tests: ink_stroke_adapter (5) + pen_note_screen widget (load + commit, 2).
flutter analyze: 0 issues. Full suite: 265/265.
This commit is contained in:
2026-06-23 10:21:40 +08:00
parent 3507e929b1
commit dfe5f2a477
12 changed files with 8769 additions and 418 deletions

View File

@@ -25,6 +25,7 @@ import '../ui/pen_settings_page.dart';
import '../ui/thumbnail_grid.dart';
import 'input_diagnostics.dart';
import 'pen_canvas.dart';
import 'pen_palette_widgets.dart';
import 'pen_stroke.dart';
/// Stable deterministic document-id for a file path (djb2 hash → hex).
@@ -444,7 +445,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
SafeArea(
child: Padding(
padding: const EdgeInsets.all(8),
child: _RoundIconButton(
child: RoundIconButton(
icon: Icons.arrow_back,
tooltip: l.back,
onPressed: () => Navigator.of(context).maybePop(),
@@ -620,42 +621,42 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_ToolButton(
ToolButton(
icon: Icons.edit_outlined,
selected: _tool == CanvasTool.pen,
tooltip: l.toolPen,
onPressed: () => setState(() => _tool = CanvasTool.pen),
),
_ToolButton(
ToolButton(
icon: Icons.brush_outlined,
selected: _tool == CanvasTool.highlighter,
tooltip: l.toolHighlighter,
onPressed: () => setState(() => _tool = CanvasTool.highlighter),
),
_ToolButton(
ToolButton(
icon: Icons.cleaning_services_outlined,
selected: _tool == CanvasTool.eraser,
tooltip: l.toolEraser,
onPressed: () => setState(() => _tool = CanvasTool.eraser),
),
_Divider(cs: cs),
PaletteDivider(cs: cs),
// Undo / redo (per page).
_ToolButton(
ToolButton(
icon: Icons.undo,
selected: false,
tooltip: l.actionUndo,
onPressed: _undoFor(_pageIndex).canUndo ? _performUndo : null,
),
_ToolButton(
ToolButton(
icon: Icons.redo,
selected: false,
tooltip: l.actionRedo,
onPressed: _undoFor(_pageIndex).canRedo ? _performRedo : null,
),
_Divider(cs: cs),
PaletteDivider(cs: cs),
for (final c in _palette) _colorDot(c, cs),
_Divider(cs: cs),
_ToolButton(
PaletteDivider(cs: cs),
ToolButton(
icon: _allowFingerDrawing ? Icons.touch_app : Icons.do_not_touch,
selected: _allowFingerDrawing,
tooltip: _allowFingerDrawing
@@ -664,20 +665,20 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
onPressed: _toggleFingerDrawing,
),
// Page thumbnail grid.
_ToolButton(
ToolButton(
icon: Icons.grid_view,
selected: false,
tooltip: l.pages,
onPressed: _document != null ? _openThumbnails : null,
),
// Pen settings.
_ToolButton(
ToolButton(
icon: Icons.settings_outlined,
selected: false,
tooltip: l.penSettings,
onPressed: _penConfig != null ? _openPenSettings : null,
),
_ToolButton(
ToolButton(
icon: Icons.bug_report_outlined,
selected: _showPenDebug,
tooltip: l.inputDiagnostic,
@@ -803,92 +804,3 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
}
}
/// A Material You toggle-style icon button for the tool palette.
class _ToolButton extends StatelessWidget {
const _ToolButton({
required this.icon,
required this.selected,
required this.tooltip,
required this.onPressed,
});
final IconData icon;
final bool selected;
final String tooltip;
/// Tap handler. When null the button renders disabled (dimmed, no ripple).
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final enabled = onPressed != null;
final iconColor = !enabled
? cs.onSurfaceVariant.withValues(alpha: 0.38)
: selected
? cs.onSecondaryContainer
: cs.onSurfaceVariant;
return Tooltip(
message: tooltip,
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: onPressed,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: selected ? cs.secondaryContainer : Colors.transparent,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
icon,
size: 22,
color: iconColor,
),
),
),
);
}
}
class _Divider extends StatelessWidget {
const _Divider({required this.cs});
final ColorScheme cs;
@override
Widget build(BuildContext context) => Container(
width: 1,
height: 24,
margin: const EdgeInsets.symmetric(horizontal: 6),
color: cs.outlineVariant,
);
}
/// A round, tonal icon button (used for the floating back button).
class _RoundIconButton extends StatelessWidget {
const _RoundIconButton({
required this.icon,
required this.tooltip,
required this.onPressed,
});
final IconData icon;
final String tooltip;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Material(
color: cs.surfaceContainerHigh,
elevation: 3,
shape: const CircleBorder(),
child: IconButton(
tooltip: tooltip,
icon: Icon(icon),
color: cs.onSurfaceVariant,
onPressed: onPressed,
),
);
}
}