feat(note): page background templates (rnote-style)
Some checks failed
CI / Windows build (push) Has been cancelled

A blank note can show a page-background template painted behind the
ink, picked from the toolbar and persisted per notebook.

- NoteBackground: blank / dots / ruled / grid / cornell, drawn in
  page space (scales with zoom), subtle grey. Cornell = left margin +
  bottom summary rule over a ruled body.
- Stored as the enum name in the notebook sidecar (back-compat:
  missing/unknown -> blank), saved/loaded via SidecarRepository so it
  restores on reopen.
- Picker added to the note tool palette.

PDF backgrounds skipped (PDFs have their own page content). analyze
clean, 386 tests green.
This commit is contained in:
2026-06-24 23:47:29 +08:00
parent 46589a4c87
commit c800295c12
6 changed files with 303 additions and 2 deletions

View File

@@ -24,6 +24,7 @@ import '../layout/viewport_fit.dart';
import '../notebook/ink_stroke_adapter.dart';
import '../ui/pen_settings_page.dart';
import 'editor_tool.dart';
import 'note_background.dart';
import 'pen_canvas.dart';
import 'pen_palette_widgets.dart';
import 'pen_stroke.dart';
@@ -83,6 +84,10 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
/// The active drawing color (the active brush's remembered color).
Color get _color => _brushColors[_activeColorBrush] ?? Colors.black;
/// The page-background template painted behind the ink (rnote-style). Default
/// blank; persisted per-notebook in the sidecar.
NoteBackground _background = NoteBackground.blank;
bool _allowFingerDrawing = false;
bool _dirty = false;
bool _needsCenter = true;
@@ -151,6 +156,7 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
if (title != null && title.isNotEmpty) {
_titleController.text = title;
}
_background = noteBackgroundFromName(repo.loadedBackground);
});
}
@@ -288,6 +294,7 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
for (final s in _strokes) EditorStroke.fromPenStroke(s),
];
repo.scheduleTitleSave(title);
repo.scheduleBackgroundSave(_background.name);
repo.scheduleStrokeSave(_notePageIndex, editorStrokes);
await repo.flush();
@@ -475,8 +482,10 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
allowFingerDrawing: _allowFingerDrawing,
onStrokeComplete: _commitStroke,
onEraseStroke: _eraseStroke,
// A white sheet with a soft shadow — the note "paper".
pageWidget: Container(
// A white sheet with a soft shadow — the note "paper" — overlaid with
// the selected background template, painted in page-pixel space (so it
// scales with zoom) and BEHIND the ink layers.
pageWidget: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
@@ -487,6 +496,10 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
),
],
),
child: CustomPaint(
painter: NoteBackgroundPainter(_background),
size: Size.infinite,
),
),
);
},
@@ -570,6 +583,55 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
PaletteDivider(cs: cs),
for (final c in _palette) _colorDot(c, cs),
PaletteDivider(cs: cs),
// Page-background template picker (rnote-style: blank / dots / ruled
// / grid / cornell). Persists per-notebook in the sidecar.
PopupMenuButton<NoteBackground>(
tooltip: 'Page background',
initialValue: _background,
onSelected: (b) {
setState(() {
_background = b;
_dirty = true;
});
},
itemBuilder: (context) => [
for (final b in NoteBackground.values)
PopupMenuItem<NoteBackground>(
value: b,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(noteBackgroundIcon(b), size: 20),
const SizedBox(width: 10),
Text(noteBackgroundLabel(b)),
if (b == _background) ...[
const SizedBox(width: 8),
Icon(Icons.check, size: 18, color: cs.primary),
],
],
),
),
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
noteBackgroundIcon(_background),
size: 22,
color: cs.onSurfaceVariant,
),
Icon(
Icons.arrow_drop_down,
size: 18,
color: cs.onSurfaceVariant,
),
],
),
),
),
PaletteDivider(cs: cs),
ToolButton(
icon: _allowFingerDrawing ? Icons.touch_app : Icons.do_not_touch,
selected: _allowFingerDrawing,