Skip to content

Commit 1db1a6f

Browse files
committed
feat: persistence of text in textbox fixed, representation still sucks
1 parent a9ddb2b commit 1db1a6f

File tree

4 files changed

+202
-220
lines changed

4 files changed

+202
-220
lines changed

lib/features/models/canvas_models/canvas_painter.dart

Lines changed: 137 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
import 'dart:convert';
44
import 'dart:math';
5-
import 'dart:ui';
6-
7-
import 'package:cookethflow/core/utils/enums.dart'; // Make sure this import is correct
5+
import 'package:cookethflow/core/utils/enums.dart';
86
import 'package:cookethflow/features/models/canvas_models/canvas_object.dart';
97
import 'package:cookethflow/features/models/canvas_models/objects/circle_object.dart';
108
import 'package:cookethflow/features/models/canvas_models/objects/cylinder_object.dart';
@@ -18,26 +16,38 @@ import 'package:cookethflow/features/models/canvas_models/objects/text_box_objec
1816
import 'package:cookethflow/features/models/canvas_models/objects/triangle_object.dart';
1917
import 'package:cookethflow/features/models/canvas_models/user_cursor.dart';
2018
import 'package:flutter/material.dart';
21-
import 'package:flutter_quill/flutter_quill.dart';
2219

2320
class CanvasPainter extends CustomPainter {
2421
final Map<String, UserCursor> userCursors;
2522
final Map<String, CanvasObject> canvasObjects;
2623
final String? currentlySelectedObjectId;
2724
final double handleRadius;
28-
final InteractionMode interactionMode; // UPDATED: Added interactionMode
25+
final InteractionMode interactionMode;
2926

3027
CanvasPainter({
3128
required this.userCursors,
3229
required this.canvasObjects,
3330
this.currentlySelectedObjectId,
3431
this.handleRadius = 8.0,
35-
required this.interactionMode, // UPDATED: Added to constructor
32+
required this.interactionMode,
3633
});
3734

35+
// Helper method to parse color from string
36+
Color _parseColor(String? colorString) {
37+
if (colorString == null) return Colors.black;
38+
try {
39+
final hex = colorString.replaceAll('#', '');
40+
if (hex.length == 6) {
41+
return Color(int.parse('FF$hex', radix: 16));
42+
}
43+
return Color(int.parse(hex, radix: 16));
44+
} catch (e) {
45+
return Colors.black;
46+
}
47+
}
48+
3849
@override
3950
void paint(Canvas canvas, Size size) {
40-
// Draw each canvas object
4151
for (final canvasObject in canvasObjects.values) {
4252
final paint = Paint()..color = canvasObject.color;
4353
Rect rect;
@@ -48,12 +58,10 @@ class CanvasPainter extends CustomPainter {
4858
center: canvasObject.center, radius: canvasObject.radius);
4959
} else if (canvasObject is TextBoxObject) {
5060
rect = canvasObject.getBounds();
51-
// FIX: Only draw the background if the color is NOT transparent.
5261
if (canvasObject.color != Colors.transparent) {
5362
canvas.drawRect(rect, paint);
5463
}
5564
} else {
56-
// For other shapes, use their getBounds() method
5765
rect = canvasObject.getBounds();
5866
if (canvasObject is Rectangle) {
5967
canvas.drawRect(rect, paint);
@@ -115,49 +123,84 @@ class CanvasPainter extends CustomPainter {
115123
}
116124
}
117125

118-
// Check if this object is currently being edited
119126
final bool isEditingText = interactionMode == InteractionMode.editingText &&
120127
currentlySelectedObjectId == canvasObject.id;
121128

122-
// Draw text content for any object that has it, but NOT if it's being edited
129+
// CHANGE: Rewrote text rendering logic for proper styling.
123130
if (canvasObject.textDelta != null &&
124131
canvasObject.textDelta!.isNotEmpty &&
125132
!isEditingText) {
126133
try {
127-
final doc = Document.fromJson(jsonDecode(canvasObject.textDelta!));
128-
final richText = TextSpan(
129-
children: doc.toDelta().map((op) {
130-
if (op.isInsert && op.data is String) {
131-
return TextSpan(
132-
text: op.data as String,
134+
final List<dynamic> deltaJson = jsonDecode(canvasObject.textDelta!);
135+
final List<TextSpan> textSpans = [];
136+
int listCounter = 1;
137+
138+
for (final op in deltaJson) {
139+
if (op is Map && op.containsKey('insert')) {
140+
String text = op['insert'];
141+
final Map<String, dynamic>? attributes =
142+
op['attributes'] as Map<String, dynamic>?;
143+
144+
double fontSize = 14.0;
145+
FontWeight fontWeight = FontWeight.normal;
146+
FontStyle fontStyle = FontStyle.normal;
147+
TextDecoration textDecoration = TextDecoration.none;
148+
Color color = Colors.black;
149+
Color? backgroundColor;
150+
String? listType;
151+
152+
if (attributes != null) {
153+
fontWeight = attributes['bold'] == true
154+
? FontWeight.bold
155+
: FontWeight.normal;
156+
fontStyle = attributes['italic'] == true
157+
? FontStyle.italic
158+
: FontStyle.normal;
159+
textDecoration = attributes['underline'] == true
160+
? TextDecoration.underline
161+
: TextDecoration.none;
162+
color = _parseColor(attributes['color'] as String?);
163+
backgroundColor =
164+
_parseColor(attributes['background'] as String?);
165+
if (attributes['header'] == 1) fontSize = 24.0;
166+
if (attributes['header'] == 2) fontSize = 20.0;
167+
if (attributes['list'] != null) {
168+
listType = attributes['list'];
169+
}
170+
}
171+
172+
if (text.endsWith('\n') && listType != null) {
173+
if (listType == 'bullet') {
174+
text = '• ${text.substring(0, text.length -1)}\n';
175+
} else if (listType == 'ordered') {
176+
text = '$listCounter. ${text.substring(0, text.length -1)}\n';
177+
listCounter++;
178+
}
179+
} else if (listType == null) {
180+
listCounter = 1; // Reset counter when not in a list
181+
}
182+
183+
textSpans.add(
184+
TextSpan(
185+
text: text,
133186
style: TextStyle(
134-
fontSize: (op.attributes?['size'] as double?) ?? 14.0,
135-
fontWeight: op.attributes?['bold'] == true
136-
? FontWeight.bold
137-
: FontWeight.normal,
138-
fontStyle: op.attributes?['italic'] == true
139-
? FontStyle.italic
140-
: FontStyle.normal,
141-
decoration: op.attributes?['underline'] == true
142-
? TextDecoration.underline
143-
: TextDecoration.none,
144-
color: Color(int.tryParse(
145-
(op.attributes?['color'] as String?)
146-
?.replaceAll('#', '0xff') ??
147-
'',
148-
radix: 16) ??
149-
Colors.black.value),
187+
fontSize: fontSize,
188+
fontWeight: fontWeight,
189+
fontStyle: fontStyle,
190+
decoration: textDecoration,
191+
color: color,
192+
backgroundColor: backgroundColor,
150193
),
151-
);
152-
}
153-
return const TextSpan();
154-
}).toList(),
155-
);
194+
),
195+
);
196+
}
197+
}
156198

199+
final richText = TextSpan(children: textSpans);
157200
final textPainter = TextPainter(
158201
text: richText,
159202
textDirection: TextDirection.ltr,
160-
textAlign: TextAlign.center, // Center text
203+
textAlign: TextAlign.start,
161204
);
162205

163206
double textPadding = 5.0;
@@ -168,8 +211,8 @@ class CanvasPainter extends CustomPainter {
168211
textPainter.layout(maxWidth: availableWidth);
169212

170213
final textOffset = Offset(
171-
rect.left + textPadding + (availableWidth - textPainter.width) / 2,
172-
rect.top + textPadding + (rect.height - 2 * textPadding - textPainter.height) / 2,
214+
rect.left + textPadding,
215+
rect.top + textPadding,
173216
);
174217

175218
canvas.save();
@@ -178,37 +221,22 @@ class CanvasPainter extends CustomPainter {
178221
canvas.restore();
179222

180223
} catch (e) {
224+
print(
225+
"Warning: Could not parse Quill Delta, rendering as plain text: $e");
181226
// Fallback for plain text
182227
final textPainter = TextPainter(
183228
text: TextSpan(
184229
text: canvasObject.textDelta,
185230
style: const TextStyle(color: Colors.black, fontSize: 14.0),
186231
),
187232
textDirection: TextDirection.ltr,
188-
textAlign: TextAlign.center,
189-
);
190-
double textPadding = 5.0;
191-
double availableWidth = rect.width - 2 * textPadding;
192-
193-
if (availableWidth <= 0) continue;
194-
195-
textPainter.layout(maxWidth: availableWidth);
196-
197-
final textOffset = Offset(
198-
rect.left + textPadding + (availableWidth - textPainter.width) / 2,
199-
rect.top + textPadding + (rect.height - 2 * textPadding - textPainter.height) / 2,
200233
);
201-
canvas.save();
202-
canvas.clipRect(rect);
203-
textPainter.paint(canvas, textOffset);
204-
canvas.restore();
205-
print("Warning: Could not parse Quill Delta, rendering as plain text: $e");
234+
textPainter.layout(maxWidth: rect.width);
235+
textPainter.paint(canvas, rect.topLeft);
206236
}
207237
}
208238

209-
210-
// Draw resize handles if this object is currently selected
211-
if (canvasObject.id == currentlySelectedObjectId) {
239+
if (canvasObject.id == currentlySelectedObjectId && !isEditingText) {
212240
final handlePaint = Paint()
213241
..color = Colors.blue
214242
..style = PaintingStyle.fill;
@@ -222,58 +250,22 @@ class CanvasPainter extends CustomPainter {
222250
..color = Colors.blue
223251
..style = PaintingStyle.stroke
224252
..strokeWidth = 2.0;
225-
226-
// Draw dashed border for transparent text box when selected
227-
if (canvasObject is TextBoxObject && canvasObject.color == Colors.transparent) {
228-
const double dashWidth = 5.0;
229-
const double dashSpace = 3.0;
230-
double currentX = rect.left;
231-
// Top line
232-
while (currentX < rect.right) {
233-
canvas.drawLine(
234-
Offset(currentX, rect.top),
235-
Offset(min(currentX + dashWidth, rect.right), rect.top),
236-
borderPaint,
237-
);
238-
currentX += dashWidth + dashSpace;
239-
}
240-
// Right line
241-
double currentY = rect.top;
242-
while (currentY < rect.bottom) {
243-
canvas.drawLine(
244-
Offset(rect.right, currentY),
245-
Offset(rect.right, min(currentY + dashWidth, rect.bottom)),
246-
borderPaint,
247-
);
248-
currentY += dashWidth + dashSpace;
249-
}
250-
// Bottom line
251-
currentX = rect.left;
252-
while (currentX < rect.right) {
253-
canvas.drawLine(
254-
Offset(rect.right - (currentX - rect.left), rect.bottom),
255-
Offset(rect.right - min((currentX - rect.left) + dashWidth, rect.width), rect.bottom),
256-
borderPaint,
257-
);
258-
currentX += dashWidth + dashSpace;
259-
}
260-
// Left line
261-
currentY = rect.top;
262-
while (currentY < rect.bottom) {
263-
canvas.drawLine(
264-
Offset(rect.left, rect.bottom - (currentY - rect.top)),
265-
Offset(rect.left, rect.bottom - min((currentY - rect.top) + dashWidth, rect.height)),
266-
borderPaint,
267-
);
268-
currentY += dashWidth + dashSpace;
269-
}
253+
254+
if (canvasObject is TextBoxObject &&
255+
canvasObject.color == Colors.transparent) {
256+
// Draw dashed border for transparent text box
257+
final path = Path()
258+
..addRect(rect);
259+
canvas.drawPath(
260+
dashPath(path, dashArray: CircularIntervalList<double>([5.0, 3.0])),
261+
borderPaint,
262+
);
270263
} else {
271264
canvas.drawRect(rect, borderPaint);
272265
}
273266
}
274267
}
275268

276-
// Draw the cursors
277269
for (final userCursor in userCursors.values) {
278270
final position = userCursor.position;
279271
final paint = Paint()..color = userCursor.color;
@@ -293,24 +285,53 @@ class CanvasPainter extends CustomPainter {
293285
return oldPainter.userCursors != userCursors ||
294286
oldPainter.canvasObjects.length != canvasObjects.length ||
295287
oldPainter.currentlySelectedObjectId != currentlySelectedObjectId ||
296-
oldPainter.interactionMode != interactionMode || // UPDATED: Add interactionMode check
288+
oldPainter.interactionMode != interactionMode ||
297289
_hasCanvasObjectsChanged(oldPainter.canvasObjects, canvasObjects);
298290
}
299291

300-
bool _hasCanvasObjectsChanged(Map<String, CanvasObject> oldObjects, Map<String, CanvasObject> newObjects) {
292+
bool _hasCanvasObjectsChanged(
293+
Map<String, CanvasObject> oldObjects, Map<String, CanvasObject> newObjects) {
301294
if (oldObjects.length != newObjects.length) return true;
302295
for (final id in newObjects.keys) {
303296
final newObj = newObjects[id];
304297
final oldObj = oldObjects[id];
305298
if (oldObj == null || newObj == null) return true;
306-
if (newObj.id != oldObj.id) return true;
307-
if (newObj.color != oldObj.color || newObj.getBounds() != oldObj.getBounds()) {
308-
return true;
309-
}
310-
if (newObj.textDelta != oldObj.textDelta) {
311-
return true;
312-
}
299+
if (newObj.getBounds() != oldObj.getBounds()) return true;
300+
if (newObj.textDelta != oldObj.textDelta) return true;
313301
}
314302
return false;
315303
}
304+
}
305+
306+
// Copied from path_drawing package to avoid adding a dependency
307+
Path dashPath(
308+
Path source, {
309+
required CircularIntervalList<double> dashArray,
310+
}) {
311+
final Path dest = Path();
312+
for (final metric in source.computeMetrics()) {
313+
double distance = 0.0;
314+
bool draw = true;
315+
while (distance < metric.length) {
316+
final len = dashArray.next;
317+
if (draw) {
318+
dest.addPath(metric.extractPath(distance, distance + len), Offset.zero);
319+
}
320+
distance += len;
321+
draw = !draw;
322+
}
323+
}
324+
return dest;
325+
}
326+
327+
class CircularIntervalList<T> {
328+
CircularIntervalList(this._values);
329+
final List<T> _values;
330+
int _idx = 0;
331+
T get next {
332+
if (_idx >= _values.length) {
333+
_idx = 0;
334+
}
335+
return _values[_idx++];
336+
}
316337
}

0 commit comments

Comments
 (0)