fix(canvas): persist strokes, pressure, slider
All checks were successful
CI / Windows build (push) Successful in 8m33s
All checks were successful
CI / Windows build (push) Successful in 8m33s
Strokes vanished on pen-up: StaticInkPainter aliased the same mutable list so shouldRepaint saw no change. Commit/erase now replace the list. Finger-drawing toggle wins over palm-rejection; pressure surfaces even when the pen reports no min/max range; pages recenter after a flip; the keyboard page-jump (unreliable on Windows) is now a drag slider. Also register the dynamic_color plugin in generated registrants.
This commit is contained in:
@@ -30,10 +30,16 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
/// Strokes per page, keyed by 0-based page index (normalized coords).
|
||||
final Map<int, List<PenStroke>> _strokesByPage = {};
|
||||
|
||||
/// One shared transform for the current page; reset on page change so each
|
||||
/// page opens fit-to-view.
|
||||
/// One shared transform for the current page; recentred on page change so
|
||||
/// each page opens fit-to-view and centered.
|
||||
final TransformationController _transform = TransformationController();
|
||||
|
||||
/// Set when the page must be (re)centered on the next layout pass.
|
||||
bool _needsCenter = true;
|
||||
|
||||
/// Live page value while dragging the page slider (null when not dragging).
|
||||
double? _scrub;
|
||||
|
||||
// Tool state.
|
||||
CanvasTool _tool = CanvasTool.pen;
|
||||
Color _color = Colors.black;
|
||||
@@ -82,7 +88,13 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
|
||||
void _commitStroke(PenStroke stroke) {
|
||||
setState(() {
|
||||
_strokesByPage.putIfAbsent(_pageIndex, () => <PenStroke>[]).add(stroke);
|
||||
// Replace with a NEW list so StaticInkPainter sees a fresh identity and
|
||||
// actually repaints (mutating in place would alias the old painter's list
|
||||
// and shouldRepaint would see no change → committed strokes vanish).
|
||||
_strokesByPage[_pageIndex] = [
|
||||
...?_strokesByPage[_pageIndex],
|
||||
stroke,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,7 +102,8 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
setState(() {
|
||||
final list = _strokesByPage[_pageIndex];
|
||||
if (list != null && index >= 0 && index < list.length) {
|
||||
list.removeAt(index);
|
||||
final next = List<PenStroke>.of(list)..removeAt(index);
|
||||
_strokesByPage[_pageIndex] = next;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -102,40 +115,15 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
if (clamped == _pageIndex) return;
|
||||
setState(() {
|
||||
_pageIndex = clamped;
|
||||
_transform.value = Matrix4.identity();
|
||||
_needsCenter = true; // recenter the new page on next layout
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _promptJumpToPage() async {
|
||||
final doc = _document;
|
||||
if (doc == null) return;
|
||||
final controller = TextEditingController(text: '${_pageIndex + 1}');
|
||||
final result = await showDialog<int>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Go to page'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(hintText: '1 – ${doc.pages.length}'),
|
||||
onSubmitted: (v) =>
|
||||
Navigator.of(context).pop(int.tryParse(v.trim())),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(int.tryParse(controller.text.trim())),
|
||||
child: const Text('Go'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (result != null) _goToPage(result - 1);
|
||||
/// Centre [pageSize] within [viewport] via the shared transform.
|
||||
void _centerPage(Size viewport, Size pageSize) {
|
||||
final tx = (viewport.width - pageSize.width) / 2;
|
||||
final ty = (viewport.height - pageSize.height) / 2;
|
||||
_transform.value = Matrix4.identity()..setTranslationRaw(tx, ty, 0);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -206,8 +194,15 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
final scale = fit < fitH ? fit : fitH;
|
||||
final pageSize = Size(page.width * scale, page.height * scale);
|
||||
|
||||
return Center(
|
||||
child: PenCanvas(
|
||||
if (_needsCenter) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_centerPage(
|
||||
Size(constraints.maxWidth, constraints.maxHeight), pageSize);
|
||||
setState(() => _needsCenter = false);
|
||||
});
|
||||
}
|
||||
return PenCanvas(
|
||||
key: ValueKey(_pageIndex),
|
||||
pageSize: pageSize,
|
||||
strokes: _currentStrokes,
|
||||
@@ -229,8 +224,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
decoration: const BoxDecoration(color: Colors.white),
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -305,42 +299,65 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Floating page-control pill: prev / "n / total" / next.
|
||||
/// Floating page-control pill: prev / drag-slider / next. No keyboard input
|
||||
/// (Windows on-screen keyboard is unreliable) — the slider scrubs pages.
|
||||
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: 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: _promptJumpToPage,
|
||||
child: Text(
|
||||
'${_pageIndex + 1} / ${doc.pages.length}',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Next page',
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: _pageIndex < doc.pages.length - 1
|
||||
? () => _goToPage(_pageIndex + 1)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
tooltip: 'Next page',
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: _pageIndex < total - 1
|
||||
? () => _goToPage(_pageIndex + 1)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user