fix: restore PDF pen capture and overhaul sticky/pens/pages
All checks were successful
CI / Windows build (push) Successful in 9m55s

Reinstall PenCaptureBinding so stylus ink hits again; keep finger Listener translucent under pinch; page-anchor sticky with drag/resize; OneNote pen slots (brush+width+color); blank-note multi-page; default side button to hold-select-text.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-07 02:38:58 +08:00
parent ad9b1b46db
commit 307161f465
16 changed files with 1279 additions and 487 deletions

View File

@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import '../../l10n/app_localizations.dart';
import '../engine/brush.dart';
import '../input/pen_slots.dart';
import 'editor_tool.dart';
/// Shared ink color palette for PDF / note / slide / scratch editors.
@@ -87,8 +88,8 @@ IconData shapeIcon(ShapeKind kind) => switch (kind) {
ShapeKind.arrow => Icons.arrow_outward,
};
/// OneNote-style pen slot: each brush is its own toolbar button with a color
/// underline (per-brush remembered color). Prefer this over [BrushPickerButton]
/// OneNote-style pen slot: each slot is its own toolbar button with a color
/// underline (slot-remembered color). Prefer this over [BrushPickerButton]
/// when the UX wants pens visible side-by-side.
class PenSlotButton extends StatelessWidget {
const PenSlotButton({
@@ -98,6 +99,7 @@ class PenSlotButton extends StatelessWidget {
required this.color,
required this.tooltip,
required this.onPressed,
this.widthHint,
});
final BrushKind kind;
@@ -106,11 +108,18 @@ class PenSlotButton extends StatelessWidget {
final String tooltip;
final VoidCallback onPressed;
/// Optional page-width fraction; when set, underline height scales slightly
/// so thicker slots read visually thicker. Null keeps the fixed 3px bar.
final double? widthHint;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final iconColor =
selected ? cs.onSecondaryContainer : cs.onSurfaceVariant;
final barHeight = widthHint == null
? 3.0
: (2.0 + (widthHint! / kThicknessLarge).clamp(0.0, 1.0) * 3.0);
return Tooltip(
message: tooltip,
child: InkWell(
@@ -131,7 +140,7 @@ class PenSlotButton extends StatelessWidget {
const SizedBox(height: 3),
Container(
width: 16,
height: 3,
height: barHeight,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
@@ -145,6 +154,119 @@ class PenSlotButton extends StatelessWidget {
}
}
/// Compact thickness control: S / M / L presets + a custom slider. Writes the
/// chosen page-width fraction via [onChanged] (typically
/// [PenSlotsController.setActiveWidth]).
class ThicknessPickerButton extends StatelessWidget {
const ThicknessPickerButton({
super.key,
required this.width,
required this.onChanged,
this.tooltip = 'Thickness',
});
/// Current stroke width (page-width fraction).
final double width;
final ValueChanged<double> onChanged;
final String tooltip;
static String _labelFor(double w) {
if ((w - kThicknessSmall).abs() < 0.0003) return 'S';
if ((w - kThicknessMedium).abs() < 0.0003) return 'M';
if ((w - kThicknessLarge).abs() < 0.0003) return 'L';
return '·';
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return PopupMenuButton<double>(
tooltip: tooltip,
onSelected: onChanged,
itemBuilder: (context) => [
PopupMenuItem<double>(
value: kThicknessSmall,
child: Row(
children: [
const Text('S'),
const Spacer(),
if ((width - kThicknessSmall).abs() < 0.0003)
Icon(Icons.check, size: 18, color: cs.primary),
],
),
),
PopupMenuItem<double>(
value: kThicknessMedium,
child: Row(
children: [
const Text('M'),
const Spacer(),
if ((width - kThicknessMedium).abs() < 0.0003)
Icon(Icons.check, size: 18, color: cs.primary),
],
),
),
PopupMenuItem<double>(
value: kThicknessLarge,
child: Row(
children: [
const Text('L'),
const Spacer(),
if ((width - kThicknessLarge).abs() < 0.0003)
Icon(Icons.check, size: 18, color: cs.primary),
],
),
),
PopupMenuItem<double>(
enabled: false,
child: SizedBox(
width: 180,
child: StatefulBuilder(
builder: (context, setLocal) {
final v = width.clamp(kPenSlotWidthMin, kPenSlotWidthMax);
return Slider(
value: v,
min: kPenSlotWidthMin,
max: kPenSlotWidthMax,
onChanged: (next) {
onChanged(next);
setLocal(() {});
},
);
},
),
),
),
],
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.line_weight, size: 20, color: cs.onSurfaceVariant),
const SizedBox(width: 2),
Text(
_labelFor(width),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: cs.onSurfaceVariant,
),
),
],
),
),
);
}
}
/// A dropdown that selects the active PEN brush (fountain / ballpoint / pencil).
///
/// Highlighter and eraser remain separate tools. Tapping the button opens a