1+ // lib/features/models/canvas_models/canvas_painter.dart
2+
13import 'dart:math' ;
4+ import 'dart:ui' ;
5+ import 'dart:convert' ; // Import for jsonDecode
6+
27import 'package:cookethflow/features/models/canvas_models/canvas_object.dart' ;
38import 'package:cookethflow/features/models/canvas_models/objects/circle_object.dart' ;
49import 'package:cookethflow/features/models/canvas_models/objects/cylinder_object.dart' ;
@@ -8,9 +13,11 @@ import 'package:cookethflow/features/models/canvas_models/objects/parallelogram_
813import 'package:cookethflow/features/models/canvas_models/objects/rectangle_object.dart' ;
914import 'package:cookethflow/features/models/canvas_models/objects/rounded_square_object.dart' ;
1015import 'package:cookethflow/features/models/canvas_models/objects/square_object.dart' ;
16+ import 'package:cookethflow/features/models/canvas_models/objects/text_box_object.dart' ; // NEW: Import TextBoxObject
1117import 'package:cookethflow/features/models/canvas_models/objects/triangle_object.dart' ;
1218import 'package:cookethflow/features/models/canvas_models/user_cursor.dart' ;
1319import 'package:flutter/material.dart' ;
20+ import 'package:flutter_quill/flutter_quill.dart' ; // NEW: Import flutter_quill
1421
1522class CanvasPainter extends CustomPainter {
1623 final Map <String , UserCursor > userCursors;
@@ -30,12 +37,26 @@ class CanvasPainter extends CustomPainter {
3037 // Draw each canvas object
3138 for (final canvasObject in canvasObjects.values) {
3239 final paint = Paint ()..color = canvasObject.color;
33-
40+
3441 Rect rect;
3542 if (canvasObject is Circle ) {
3643 canvas.drawCircle (canvasObject.center, canvasObject.radius, paint);
3744 rect = Rect .fromCircle (center: canvasObject.center, radius: canvasObject.radius);
38- } else {
45+ } else if (canvasObject is TextBoxObject ) { // NEW: Handle TextBoxObject drawing
46+ rect = canvasObject.getBounds ();
47+ // For text boxes, we might draw a subtle border if it's transparent,
48+ // or just let the text render.
49+ if (canvasObject.color == Colors .transparent) {
50+ final borderPaint = Paint ()
51+ ..color = Colors .grey.shade400
52+ ..style = PaintingStyle .stroke
53+ ..strokeWidth = 1.0 ;
54+ canvas.drawRect (rect, borderPaint);
55+ } else {
56+ canvas.drawRect (rect, paint);
57+ }
58+ }
59+ else {
3960 // For other shapes, use their getBounds() method
4061 rect = canvasObject.getBounds ();
4162 if (canvasObject is Rectangle ) {
@@ -84,6 +105,94 @@ class CanvasPainter extends CustomPainter {
84105 }
85106 }
86107
108+ // NEW: Draw text content for any object that has it
109+ if (canvasObject.textDelta != null && canvasObject.textDelta! .isNotEmpty) {
110+ try {
111+ final doc = Document .fromJson (jsonDecode (canvasObject.textDelta! ));
112+ final richText = TextSpan (
113+ children: doc.toDelta ().map ((op) {
114+ if (op.isInsert && op.data is String ) {
115+ return TextSpan (
116+ text: op.data as String ,
117+ style: TextStyle (
118+ fontSize: (op.attributes? ['size' ] as double ? ) ?? 14.0 , // Default font size
119+ fontWeight: op.attributes? ['bold' ] == true ? FontWeight .bold : FontWeight .normal,
120+ fontStyle: op.attributes? ['italic' ] == true ? FontStyle .italic : FontStyle .normal,
121+ decoration: op.attributes? ['underline' ] == true ? TextDecoration .underline : TextDecoration .none,
122+ color: Color (int .tryParse ((op.attributes? ['color' ] as String ? )? .replaceAll ('#' , '0xff' ) ?? '' , radix: 16 ) ?? Colors .black.value),
123+ ),
124+ );
125+ }
126+ return const TextSpan ();
127+ }).toList (),
128+ );
129+
130+ final textPainter = TextPainter (
131+ text: richText,
132+ textDirection: TextDirection .ltr, // Assuming LTR for most cases
133+ maxLines: null , // Allow multiple lines
134+ );
135+
136+ // Constrain text to object's bounds.
137+ // For TextBoxObject, the text fills the box.
138+ // For other shapes, it can be centered with some padding.
139+ double textPadding = 5.0 ; // Small padding inside shapes
140+ double availableWidth = rect.width - 2 * textPadding;
141+ double availableHeight = rect.height - 2 * textPadding;
142+
143+ if (availableWidth <= 0 || availableHeight <= 0 ) {
144+ continue ; // Skip drawing text if object is too small
145+ }
146+
147+ textPainter.layout (maxWidth: availableWidth);
148+
149+ // Calculate offset to center text vertically and horizontally
150+ final textOffset = Offset (
151+ rect.left + textPadding + (availableWidth - textPainter.width) / 2 ,
152+ rect.top + textPadding + (availableHeight - textPainter.height) / 2 ,
153+ );
154+
155+ // Save canvas state before clipping, restore after
156+ canvas.save ();
157+ // Clip text to the object's bounds to prevent overflow
158+ canvas.clipRect (rect);
159+ textPainter.paint (canvas, textOffset);
160+ canvas.restore ();
161+
162+ } catch (e) {
163+ // Fallback for malformed Quill Delta (or plain text directly saved)
164+ final textPainter = TextPainter (
165+ text: TextSpan (
166+ text: canvasObject.textDelta, // Render as plain text
167+ style: const TextStyle (color: Colors .black, fontSize: 14.0 ),
168+ ),
169+ textDirection: TextDirection .ltr,
170+ maxLines: null ,
171+ );
172+
173+ double textPadding = 5.0 ;
174+ double availableWidth = rect.width - 2 * textPadding;
175+ double availableHeight = rect.height - 2 * textPadding;
176+
177+ if (availableWidth <= 0 || availableHeight <= 0 ) {
178+ continue ;
179+ }
180+
181+ textPainter.layout (maxWidth: availableWidth);
182+
183+ final textOffset = Offset (
184+ rect.left + textPadding + (availableWidth - textPainter.width) / 2 ,
185+ rect.top + textPadding + (availableHeight - textPainter.height) / 2 ,
186+ );
187+ canvas.save ();
188+ canvas.clipRect (rect);
189+ textPainter.paint (canvas, textOffset);
190+ canvas.restore ();
191+ print ("Warning: Could not parse Quill Delta, rendering as plain text: $e " );
192+ }
193+ }
194+
195+
87196 // Draw resize handles if this object is currently selected
88197 if (canvasObject.id == currentlySelectedObjectId) {
89198 final handlePaint = Paint ()
@@ -96,12 +205,63 @@ class CanvasPainter extends CustomPainter {
96205 canvas.drawCircle (rect.bottomLeft, handleRadius, handlePaint);
97206 canvas.drawCircle (rect.bottomRight, handleRadius, handlePaint);
98207
99- // Draw selection border
208+ // Draw selection border (dashed for text box if transparent)
100209 final borderPaint = Paint ()
101210 ..color = Colors .blue
102211 ..style = PaintingStyle .stroke
103212 ..strokeWidth = 2.0 ;
104- canvas.drawRect (rect, borderPaint);
213+
214+ if (canvasObject is TextBoxObject && canvasObject.color == Colors .transparent) {
215+ // Draw dashed border for transparent text box
216+ const double dashWidth = 5.0 ;
217+ const double dashSpace = 3.0 ;
218+ double currentX = rect.left;
219+ double currentY = rect.top;
220+
221+ // Top line
222+ while (currentX < rect.right) {
223+ canvas.drawLine (
224+ Offset (currentX, rect.top),
225+ Offset (min (currentX + dashWidth, rect.right), rect.top),
226+ borderPaint,
227+ );
228+ currentX += dashWidth + dashSpace;
229+ }
230+ // Right line
231+ currentX = rect.right;
232+ while (currentY < rect.bottom) {
233+ canvas.drawLine (
234+ Offset (rect.right, currentY),
235+ Offset (rect.right, min (currentY + dashWidth, rect.bottom)),
236+ borderPaint,
237+ );
238+ currentY += dashWidth + dashSpace;
239+ }
240+ // Bottom line
241+ currentY = rect.bottom;
242+ currentX = rect.right;
243+ while (currentX > rect.left) {
244+ canvas.drawLine (
245+ Offset (currentX, rect.bottom),
246+ Offset (max (currentX - dashWidth, rect.left), rect.bottom),
247+ borderPaint,
248+ );
249+ currentX -= dashWidth + dashSpace;
250+ }
251+ // Left line
252+ currentX = rect.left;
253+ currentY = rect.bottom;
254+ while (currentY > rect.top) {
255+ canvas.drawLine (
256+ Offset (rect.left, currentY),
257+ Offset (rect.left, max (currentY - dashWidth, rect.top)),
258+ borderPaint,
259+ );
260+ currentY -= dashWidth + dashSpace;
261+ }
262+ } else {
263+ canvas.drawRect (rect, borderPaint);
264+ }
105265 }
106266 }
107267
@@ -122,8 +282,41 @@ class CanvasPainter extends CustomPainter {
122282
123283 @override
124284 bool shouldRepaint (CanvasPainter oldPainter) {
285+ // Only repaint if the data or selection changes significantly
125286 return oldPainter.userCursors != userCursors ||
126- oldPainter.canvasObjects != canvasObjects ||
127- oldPainter.currentlySelectedObjectId != currentlySelectedObjectId;
287+ oldPainter.canvasObjects.length != canvasObjects.length ||
288+ oldPainter.currentlySelectedObjectId != currentlySelectedObjectId ||
289+ // Deep comparison of canvas objects is expensive, but necessary if text content changes frequently
290+ _hasCanvasObjectsChanged (oldPainter.canvasObjects, canvasObjects);
291+ }
292+
293+ // Helper method for deep comparison of canvas objects
294+ bool _hasCanvasObjectsChanged (Map <String , CanvasObject > oldObjects, Map <String , CanvasObject > newObjects) {
295+ if (oldObjects.length != newObjects.length) return true ;
296+
297+ for (final id in newObjects.keys) {
298+ final newObj = newObjects[id];
299+ final oldObj = oldObjects[id];
300+
301+ if (oldObj == null || newObj == null ) return true ; // Object added/removed
302+
303+ // Check if ID is different (shouldn't happen for same key)
304+ if (newObj.id != oldObj.id) return true ;
305+
306+ // Check basic properties
307+ if (newObj.color != oldObj.color ||
308+ newObj.getBounds () != oldObj.getBounds ()) {
309+ return true ;
310+ }
311+
312+ // Check textDelta content
313+ if (newObj.textDelta != oldObj.textDelta) {
314+ return true ;
315+ }
316+ // Add more specific checks if objects have other unique properties that can change.
317+ // For instance, if circle's radius or center changed.
318+ // A more robust check might involve comparing their toJson() output.
319+ }
320+ return false ;
128321 }
129322}
0 commit comments