fix(canvas): compact page pill + pen diagnostic
All checks were successful
CI / Windows build (push) Successful in 10m45s

Page slider is no longer persistent: a compact prev/'n/total'/next pill;
tapping the label reveals the slider (collapses again), so it stops
blocking the page. Add a pen-pressure diagnostic toggle (bug icon) that
shows the live kind/pressure/min/max Windows delivers — to pin down why
pressure reads flat on the Surface Pen.
This commit is contained in:
2026-06-21 23:13:05 +08:00
parent 0c40a456e8
commit 1e2a83b0b9
2 changed files with 134 additions and 52 deletions

View File

@@ -42,8 +42,14 @@ class PenCanvas extends StatefulWidget {
this.allowFingerDrawing = false,
this.minScale = 0.5,
this.maxScale = 8.0,
this.onPenDebug,
});
/// Debug hook: called with a readout of the latest pen event
/// (kind / pressure / pressureMin / pressureMax) so we can see what Windows
/// actually delivers. Null in release UI.
final void Function(String readout)? onPenDebug;
/// The rendered PDF page bitmap, already sized to [pageSize].
final Widget pageWidget;
@@ -249,8 +255,18 @@ class _PenCanvasState extends State<PenCanvas> {
// --- Listener callbacks ---------------------------------------------------
void _emitPenDebug(PointerEvent event) {
final cb = widget.onPenDebug;
if (cb == null) return;
cb('${event.kind.name} p=${event.pressure.toStringAsFixed(3)} '
'min=${event.pressureMin.toStringAsFixed(2)} '
'max=${event.pressureMax.toStringAsFixed(2)} '
'tilt=${event.tilt.toStringAsFixed(2)}');
}
void _onPointerHover(PointerHoverEvent event) {
if (_isStylus(event.kind)) {
_emitPenDebug(event);
// Detect eraser (barrel button / inverted) while hovering.
_eraserActive = _isEraserSignal(event);
}
@@ -258,6 +274,7 @@ class _PenCanvasState extends State<PenCanvas> {
void _onPointerDown(PointerDownEvent event) {
if (event.kind == PointerDeviceKind.trackpad) return;
if (_isStylus(event.kind)) _emitPenDebug(event);
_activePointers[event.pointer] = event.kind;
@@ -282,6 +299,7 @@ class _PenCanvasState extends State<PenCanvas> {
}
void _onPointerMove(PointerMoveEvent event) {
if (_isStylus(event.kind)) _emitPenDebug(event);
if (event.pointer != _drawPointer) return;
if (_activePointers.length >= 2) return; // pinch owns it
_extendStroke(event);

View File

@@ -40,6 +40,15 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
/// Live page value while dragging the page slider (null when not dragging).
double? _scrub;
/// Whether the page-jump slider is expanded (NOT persistent — toggled by
/// tapping the page label; collapses after a jump).
bool _showSlider = false;
/// Latest pen-event debug readout (kind/pressure/min/max) — shown only when
/// the diagnostic toggle is on, to inspect what Windows delivers.
String _penDebug = '';
bool _showPenDebug = false;
// Tool state.
CanvasTool _tool = CanvasTool.pen;
Color _color = Colors.black;
@@ -164,6 +173,34 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
),
),
),
// Pen diagnostic readout (top-right) — shows what Windows delivers.
if (_showPenDebug)
SafeArea(
child: Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(8),
child: Material(
color: Theme.of(context).colorScheme.inverseSurface,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
child: Text(
_penDebug.isEmpty
? 'hover / draw with the pen…'
: _penDebug,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Theme.of(context).colorScheme.onInverseSurface,
),
),
),
),
),
),
),
],
),
);
@@ -213,6 +250,9 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
? _highlighterWidthFraction
: _penWidthFraction,
allowFingerDrawing: _allowFingerDrawing,
onPenDebug: _showPenDebug
? (s) => setState(() => _penDebug = s)
: null,
onStrokeComplete: _commitStroke,
onEraseStroke: _eraseStroke,
pageWidget: PdfPageView(
@@ -272,6 +312,12 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
onPressed: () =>
setState(() => _allowFingerDrawing = !_allowFingerDrawing),
),
_ToolButton(
icon: Icons.bug_report_outlined,
selected: _showPenDebug,
tooltip: 'Pen pressure diagnostic',
onPressed: () => setState(() => _showPenDebug = !_showPenDebug),
),
],
),
),
@@ -299,67 +345,85 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
);
}
/// Floating page-control pill: prev / drag-slider / next. No keyboard input
/// (Windows on-screen keyboard is unreliable) — the slider scrubs pages.
/// Floating page control: a COMPACT pill (prev / "n / total" / next). Tapping
/// the label reveals a drag-slider — which is NOT persistent (collapses again
/// on tap) so it doesn't block the page. No keyboard input (Windows IME is
/// unreliable).
Widget _buildPagePill() {
final doc = _document!;
final cs = Theme.of(context).colorScheme;
final total = doc.pages.length;
final shown = (_scrub ?? (_pageIndex + 1).toDouble()).round();
return Material(
color: cs.surfaceContainerHigh,
elevation: 3,
borderRadius: BorderRadius.circular(28),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'Previous page',
icon: const Icon(Icons.chevron_left),
onPressed:
_pageIndex > 0 ? () => _goToPage(_pageIndex - 1) : null,
),
if (total > 1)
Flexible(
child: Slider(
min: 1,
max: total.toDouble(),
value: (_scrub ?? (_pageIndex + 1).toDouble())
.clamp(1, total.toDouble()),
label: '$shown',
divisions: total - 1,
onChanged: (v) => setState(() => _scrub = v),
onChangeEnd: (v) {
setState(() => _scrub = null);
_goToPage(v.round() - 1);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Text(
'$shown / $total',
style: TextStyle(
color: cs.onSurface,
fontWeight: FontWeight.w600,
),
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Slider — shown only when expanded (not persistent).
if (_showSlider && total > 1)
Container(
margin: const EdgeInsets.only(bottom: 8),
constraints: const BoxConstraints(maxWidth: 420),
child: Material(
color: cs.surfaceContainerHigh,
elevation: 3,
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Slider(
min: 1,
max: total.toDouble(),
value: (_scrub ?? (_pageIndex + 1).toDouble())
.clamp(1, total.toDouble()),
label: '$shown',
divisions: total - 1,
onChanged: (v) => setState(() => _scrub = v),
onChangeEnd: (v) {
setState(() => _scrub = null);
_goToPage(v.round() - 1);
},
),
),
IconButton(
tooltip: 'Next page',
icon: const Icon(Icons.chevron_right),
onPressed: _pageIndex < total - 1
? () => _goToPage(_pageIndex + 1)
: null,
),
],
),
),
// Compact pill — always; fits content (no big frame).
Material(
color: cs.surfaceContainerHigh,
elevation: 3,
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'Previous page',
icon: const Icon(Icons.chevron_left),
onPressed:
_pageIndex > 0 ? () => _goToPage(_pageIndex - 1) : null,
),
TextButton(
onPressed: total > 1
? () => setState(() => _showSlider = !_showSlider)
: null,
child: Text(
'$shown / $total',
style: TextStyle(
color: cs.onSurface,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
tooltip: 'Next page',
icon: const Icon(Icons.chevron_right),
onPressed: _pageIndex < total - 1
? () => _goToPage(_pageIndex + 1)
: null,
),
],
),
),
),
),
],
);
}
}