feat(pdf): typed-text tool (Windows-Ink friendly)
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
Add a text-annotation tool to the PDF editor. With the text tool a
pen-tap, or a mouse double-click, drops a text box at that normalized
page point and focuses a real Flutter TextField — so the OS IME and the
Windows-Ink handwriting panel feed it (device-validated). Tapping an
existing box re-opens it; clearing it deletes it.
- SidecarText {nx, ny, text, fontSize (page-relative), color} per page,
glued under zoom; stored in the sidecar `texts` field (back-compat
missing -> none), saved via scheduleTextsSave and loaded on open.
- Rendered in pageOverlaysBuilder at the scaled position.
PDF editor only for now (note text later). analyze clean, 397 tests.
This commit is contained in:
@@ -108,6 +108,95 @@ class SidecarHighlight {
|
||||
'SidecarHighlight(l: $l, t: $t, r: $r, b: $b, color: $color)';
|
||||
}
|
||||
|
||||
/// A single typed-text annotation on a page (PDF editor for now). Position is
|
||||
/// NORMALIZED to the page rect ([nx],[ny] in [0,1]) so the box stays glued under
|
||||
/// zoom/scroll, exactly like [SidecarHighlight] / [ScratchLink]. [fontSize] is
|
||||
/// PAGE-RELATIVE (a fraction of the page width), so the rendered text scales
|
||||
/// with the page; the editor multiplies it by the on-screen page width.
|
||||
class SidecarText {
|
||||
const SidecarText({
|
||||
required this.id,
|
||||
required this.nx,
|
||||
required this.ny,
|
||||
required this.text,
|
||||
this.fontSize = 0.03,
|
||||
this.color = 0xFF000000,
|
||||
});
|
||||
|
||||
/// Stable id (uuid) so edits/deletes address a specific box.
|
||||
final String id;
|
||||
|
||||
/// Normalized x of the box's top-left in [0,1].
|
||||
final double nx;
|
||||
|
||||
/// Normalized y of the box's top-left in [0,1].
|
||||
final double ny;
|
||||
|
||||
/// The typed text.
|
||||
final String text;
|
||||
|
||||
/// Font size as a fraction of page WIDTH (page-relative; scales with zoom).
|
||||
final double fontSize;
|
||||
|
||||
/// ARGB text color.
|
||||
final int color;
|
||||
|
||||
SidecarText copyWith({
|
||||
String? id,
|
||||
double? nx,
|
||||
double? ny,
|
||||
String? text,
|
||||
double? fontSize,
|
||||
int? color,
|
||||
}) =>
|
||||
SidecarText(
|
||||
id: id ?? this.id,
|
||||
nx: nx ?? this.nx,
|
||||
ny: ny ?? this.ny,
|
||||
text: text ?? this.text,
|
||||
fontSize: fontSize ?? this.fontSize,
|
||||
color: color ?? this.color,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'nx': nx,
|
||||
'ny': ny,
|
||||
'text': text,
|
||||
'fontSize': fontSize,
|
||||
'color': color,
|
||||
};
|
||||
|
||||
factory SidecarText.fromJson(Map<String, dynamic> json) => SidecarText(
|
||||
id: json['id'] as String,
|
||||
nx: (json['nx'] as num).toDouble(),
|
||||
ny: (json['ny'] as num).toDouble(),
|
||||
text: (json['text'] as String?) ?? '',
|
||||
fontSize: (json['fontSize'] as num?)?.toDouble() ?? 0.03,
|
||||
color: (json['color'] as num?)?.toInt() ?? 0xFF000000,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SidecarText &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
nx == other.nx &&
|
||||
ny == other.ny &&
|
||||
text == other.text &&
|
||||
fontSize == other.fontSize &&
|
||||
color == other.color;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, nx, ny, text, fontSize, color);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'SidecarText(id: $id, nx: $nx, ny: $ny, text: $text, '
|
||||
'fontSize: $fontSize, color: $color)';
|
||||
}
|
||||
|
||||
/// An anchor's private infinite scratchpad: a list of [InkStroke]s in ABSOLUTE
|
||||
/// world pixels (unchanged format from `SplitViewScreen`), plus the world size
|
||||
/// so it restores (today the canvas always resets to 4000×4000).
|
||||
@@ -219,6 +308,7 @@ class BadnoteSidecar {
|
||||
this.updatedAt,
|
||||
Map<int, List<EditorStroke>>? strokes,
|
||||
Map<int, List<SidecarHighlight>>? highlights,
|
||||
Map<int, List<SidecarText>>? texts,
|
||||
List<Bookmark>? bookmarks,
|
||||
List<SidecarScratchLink>? scratchLinks,
|
||||
Map<int, String>? legacyAnnotations,
|
||||
@@ -227,6 +317,7 @@ class BadnoteSidecar {
|
||||
this.background,
|
||||
}) : strokes = strokes ?? <int, List<EditorStroke>>{},
|
||||
highlights = highlights ?? <int, List<SidecarHighlight>>{},
|
||||
texts = texts ?? <int, List<SidecarText>>{},
|
||||
bookmarks = bookmarks ?? <Bookmark>[],
|
||||
scratchLinks = scratchLinks ?? <SidecarScratchLink>[],
|
||||
legacyAnnotations = legacyAnnotations ?? <int, String>{};
|
||||
@@ -255,6 +346,10 @@ class BadnoteSidecar {
|
||||
/// Page index → highlighted text rects (normalized).
|
||||
final Map<int, List<SidecarHighlight>> highlights;
|
||||
|
||||
/// Page index → typed-text annotations (normalized position, page-relative
|
||||
/// font size). PDF editor only for now (note text is a later increment).
|
||||
final Map<int, List<SidecarText>> texts;
|
||||
|
||||
final List<Bookmark> bookmarks;
|
||||
final List<SidecarScratchLink> scratchLinks;
|
||||
|
||||
@@ -304,6 +399,12 @@ class BadnoteSidecar {
|
||||
entry.key.toString():
|
||||
entry.value.map((h) => h.toJson()).toList(),
|
||||
},
|
||||
if (texts.isNotEmpty)
|
||||
'texts': {
|
||||
for (final entry in texts.entries)
|
||||
entry.key.toString():
|
||||
entry.value.map((t) => t.toJson()).toList(),
|
||||
},
|
||||
'bookmarks': bookmarks.map((b) => b.toJson()).toList(),
|
||||
'scratchLinks': scratchLinks.map((s) => s.toJson()).toList(),
|
||||
if (legacyAnnotations.isNotEmpty)
|
||||
@@ -350,6 +451,7 @@ class BadnoteSidecar {
|
||||
: DateTime.tryParse(json['updatedAt'] as String),
|
||||
strokes: decodePageMap(json['strokes'], EditorStroke.fromJson),
|
||||
highlights: decodePageMap(json['highlights'], SidecarHighlight.fromJson),
|
||||
texts: decodePageMap(json['texts'], SidecarText.fromJson),
|
||||
bookmarks: ((json['bookmarks'] as List<dynamic>?) ?? const [])
|
||||
.map((e) => Bookmark.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
|
||||
Reference in New Issue
Block a user