22
33import 'dart:convert' ;
44import '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' ;
86import 'package:cookethflow/features/models/canvas_models/canvas_object.dart' ;
97import 'package:cookethflow/features/models/canvas_models/objects/circle_object.dart' ;
108import '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
1816import 'package:cookethflow/features/models/canvas_models/objects/triangle_object.dart' ;
1917import 'package:cookethflow/features/models/canvas_models/user_cursor.dart' ;
2018import 'package:flutter/material.dart' ;
21- import 'package:flutter_quill/flutter_quill.dart' ;
2219
2320class 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