Skip to content

Commit a8e6bf4

Browse files
committed
feat: text box feature
1 parent 54a7189 commit a8e6bf4

File tree

4 files changed

+331
-271
lines changed

4 files changed

+331
-271
lines changed

lib/features/models/canvas_models/canvas_painter.dart

Lines changed: 84 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// lib/features/models/canvas_models/canvas_painter.dart
22

3+
import 'dart:convert';
34
import 'dart:math';
45
import 'dart:ui';
5-
import 'dart:convert'; // Import for jsonDecode
66

7+
import 'package:cookethflow/core/utils/enums.dart'; // Make sure this import is correct
78
import 'package:cookethflow/features/models/canvas_models/canvas_object.dart';
89
import 'package:cookethflow/features/models/canvas_models/objects/circle_object.dart';
910
import 'package:cookethflow/features/models/canvas_models/objects/cylinder_object.dart';
@@ -13,58 +14,56 @@ import 'package:cookethflow/features/models/canvas_models/objects/parallelogram_
1314
import 'package:cookethflow/features/models/canvas_models/objects/rectangle_object.dart';
1415
import 'package:cookethflow/features/models/canvas_models/objects/rounded_square_object.dart';
1516
import '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
17+
import 'package:cookethflow/features/models/canvas_models/objects/text_box_object.dart';
1718
import 'package:cookethflow/features/models/canvas_models/objects/triangle_object.dart';
1819
import 'package:cookethflow/features/models/canvas_models/user_cursor.dart';
1920
import 'package:flutter/material.dart';
20-
import 'package:flutter_quill/flutter_quill.dart'; // NEW: Import flutter_quill
21+
import 'package:flutter_quill/flutter_quill.dart';
2122

2223
class CanvasPainter extends CustomPainter {
2324
final Map<String, UserCursor> userCursors;
2425
final Map<String, CanvasObject> canvasObjects;
2526
final String? currentlySelectedObjectId;
2627
final double handleRadius;
28+
final InteractionMode interactionMode; // UPDATED: Added interactionMode
2729

2830
CanvasPainter({
2931
required this.userCursors,
3032
required this.canvasObjects,
3133
this.currentlySelectedObjectId,
3234
this.handleRadius = 8.0,
35+
required this.interactionMode, // UPDATED: Added to constructor
3336
});
3437

3538
@override
3639
void paint(Canvas canvas, Size size) {
3740
// Draw each canvas object
3841
for (final canvasObject in canvasObjects.values) {
3942
final paint = Paint()..color = canvasObject.color;
40-
4143
Rect rect;
44+
4245
if (canvasObject is Circle) {
4346
canvas.drawCircle(canvasObject.center, canvasObject.radius, paint);
44-
rect = Rect.fromCircle(center: canvasObject.center, radius: canvasObject.radius);
45-
} else if (canvasObject is TextBoxObject) { // NEW: Handle TextBoxObject drawing
47+
rect = Rect.fromCircle(
48+
center: canvasObject.center, radius: canvasObject.radius);
49+
} else if (canvasObject is TextBoxObject) {
4650
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 {
51+
// FIX: Only draw the background if the color is NOT transparent.
52+
if (canvasObject.color != Colors.transparent) {
5653
canvas.drawRect(rect, paint);
5754
}
58-
}
59-
else {
55+
} else {
6056
// For other shapes, use their getBounds() method
6157
rect = canvasObject.getBounds();
6258
if (canvasObject is Rectangle) {
6359
canvas.drawRect(rect, paint);
6460
} else if (canvasObject is Square) {
6561
canvas.drawRect(rect, paint);
6662
} else if (canvasObject is RoundedSquare) {
67-
canvas.drawRRect(RRect.fromRectAndRadius(rect, Radius.circular(canvasObject.cornerRadius)), paint);
63+
canvas.drawRRect(
64+
RRect.fromRectAndRadius(
65+
rect, Radius.circular(canvasObject.cornerRadius)),
66+
paint);
6867
} else if (canvasObject is Diamond) {
6968
final path = Path()
7069
..moveTo(rect.center.dx, rect.top)
@@ -98,15 +97,32 @@ class CanvasPainter extends CustomPainter {
9897
canvas.drawPath(path, paint);
9998
} else if (canvasObject is Cylinder) {
10099
final ellipseHeight = min(rect.height * 0.3, 40.0);
101-
final bodyRect = Rect.fromLTRB(rect.left, rect.top + ellipseHeight / 2, rect.right, rect.bottom - ellipseHeight / 2);
100+
final bodyRect = Rect.fromLTRB(rect.left,
101+
rect.top + ellipseHeight / 2, rect.right, rect.bottom - ellipseHeight / 2);
102102
canvas.drawRect(bodyRect, paint);
103-
canvas.drawOval(Rect.fromCenter(center: bodyRect.topCenter, width: rect.width, height: ellipseHeight), paint);
104-
canvas.drawOval(Rect.fromCenter(center: bodyRect.bottomCenter, width: rect.width, height: ellipseHeight), paint);
103+
canvas.drawOval(
104+
Rect.fromCenter(
105+
center: bodyRect.topCenter,
106+
width: rect.width,
107+
height: ellipseHeight),
108+
paint);
109+
canvas.drawOval(
110+
Rect.fromCenter(
111+
center: bodyRect.bottomCenter,
112+
width: rect.width,
113+
height: ellipseHeight),
114+
paint);
105115
}
106116
}
107117

108-
// NEW: Draw text content for any object that has it
109-
if (canvasObject.textDelta != null && canvasObject.textDelta!.isNotEmpty) {
118+
// Check if this object is currently being edited
119+
final bool isEditingText = interactionMode == InteractionMode.editingText &&
120+
currentlySelectedObjectId == canvasObject.id;
121+
122+
// Draw text content for any object that has it, but NOT if it's being edited
123+
if (canvasObject.textDelta != null &&
124+
canvasObject.textDelta!.isNotEmpty &&
125+
!isEditingText) {
110126
try {
111127
final doc = Document.fromJson(jsonDecode(canvasObject.textDelta!));
112128
final richText = TextSpan(
@@ -115,11 +131,22 @@ class CanvasPainter extends CustomPainter {
115131
return TextSpan(
116132
text: op.data as String,
117133
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),
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),
123150
),
124151
);
125152
}
@@ -129,60 +156,47 @@ class CanvasPainter extends CustomPainter {
129156

130157
final textPainter = TextPainter(
131158
text: richText,
132-
textDirection: TextDirection.ltr, // Assuming LTR for most cases
133-
maxLines: null, // Allow multiple lines
159+
textDirection: TextDirection.ltr,
160+
textAlign: TextAlign.center, // Center text
134161
);
135162

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
163+
double textPadding = 5.0;
140164
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-
}
165+
166+
if (availableWidth <= 0) continue;
146167

147168
textPainter.layout(maxWidth: availableWidth);
148-
149-
// Calculate offset to center text vertically and horizontally
169+
150170
final textOffset = Offset(
151171
rect.left + textPadding + (availableWidth - textPainter.width) / 2,
152-
rect.top + textPadding + (availableHeight - textPainter.height) / 2,
172+
rect.top + textPadding + (rect.height - 2 * textPadding - textPainter.height) / 2,
153173
);
154174

155-
// Save canvas state before clipping, restore after
156175
canvas.save();
157-
// Clip text to the object's bounds to prevent overflow
158176
canvas.clipRect(rect);
159177
textPainter.paint(canvas, textOffset);
160178
canvas.restore();
161179

162180
} catch (e) {
163-
// Fallback for malformed Quill Delta (or plain text directly saved)
181+
// Fallback for plain text
164182
final textPainter = TextPainter(
165183
text: TextSpan(
166-
text: canvasObject.textDelta, // Render as plain text
184+
text: canvasObject.textDelta,
167185
style: const TextStyle(color: Colors.black, fontSize: 14.0),
168186
),
169187
textDirection: TextDirection.ltr,
170-
maxLines: null,
188+
textAlign: TextAlign.center,
171189
);
172-
173190
double textPadding = 5.0;
174191
double availableWidth = rect.width - 2 * textPadding;
175-
double availableHeight = rect.height - 2 * textPadding;
176192

177-
if (availableWidth <= 0 || availableHeight <= 0) {
178-
continue;
179-
}
193+
if (availableWidth <= 0) continue;
180194

181195
textPainter.layout(maxWidth: availableWidth);
182196

183197
final textOffset = Offset(
184198
rect.left + textPadding + (availableWidth - textPainter.width) / 2,
185-
rect.top + textPadding + (availableHeight - textPainter.height) / 2,
199+
rect.top + textPadding + (rect.height - 2 * textPadding - textPainter.height) / 2,
186200
);
187201
canvas.save();
188202
canvas.clipRect(rect);
@@ -198,26 +212,22 @@ class CanvasPainter extends CustomPainter {
198212
final handlePaint = Paint()
199213
..color = Colors.blue
200214
..style = PaintingStyle.fill;
201-
202-
// Draw corner handles
215+
203216
canvas.drawCircle(rect.topLeft, handleRadius, handlePaint);
204217
canvas.drawCircle(rect.topRight, handleRadius, handlePaint);
205218
canvas.drawCircle(rect.bottomLeft, handleRadius, handlePaint);
206219
canvas.drawCircle(rect.bottomRight, handleRadius, handlePaint);
207220

208-
// Draw selection border (dashed for text box if transparent)
209221
final borderPaint = Paint()
210222
..color = Colors.blue
211223
..style = PaintingStyle.stroke
212224
..strokeWidth = 2.0;
213-
225+
226+
// Draw dashed border for transparent text box when selected
214227
if (canvasObject is TextBoxObject && canvasObject.color == Colors.transparent) {
215-
// Draw dashed border for transparent text box
216228
const double dashWidth = 5.0;
217229
const double dashSpace = 3.0;
218230
double currentX = rect.left;
219-
double currentY = rect.top;
220-
221231
// Top line
222232
while (currentX < rect.right) {
223233
canvas.drawLine(
@@ -228,7 +238,7 @@ class CanvasPainter extends CustomPainter {
228238
currentX += dashWidth + dashSpace;
229239
}
230240
// Right line
231-
currentX = rect.right;
241+
double currentY = rect.top;
232242
while (currentY < rect.bottom) {
233243
canvas.drawLine(
234244
Offset(rect.right, currentY),
@@ -238,26 +248,24 @@ class CanvasPainter extends CustomPainter {
238248
currentY += dashWidth + dashSpace;
239249
}
240250
// Bottom line
241-
currentY = rect.bottom;
242-
currentX = rect.right;
243-
while (currentX > rect.left) {
251+
currentX = rect.left;
252+
while (currentX < rect.right) {
244253
canvas.drawLine(
245-
Offset(currentX, rect.bottom),
246-
Offset(max(currentX - dashWidth, rect.left), rect.bottom),
254+
Offset(rect.right - (currentX - rect.left), rect.bottom),
255+
Offset(rect.right - min((currentX - rect.left) + dashWidth, rect.width), rect.bottom),
247256
borderPaint,
248257
);
249-
currentX -= dashWidth + dashSpace;
258+
currentX += dashWidth + dashSpace;
250259
}
251260
// Left line
252-
currentX = rect.left;
253-
currentY = rect.bottom;
254-
while (currentY > rect.top) {
261+
currentY = rect.top;
262+
while (currentY < rect.bottom) {
255263
canvas.drawLine(
256-
Offset(rect.left, currentY),
257-
Offset(rect.left, max(currentY - dashWidth, rect.top)),
264+
Offset(rect.left, rect.bottom - (currentY - rect.top)),
265+
Offset(rect.left, rect.bottom - min((currentY - rect.top) + dashWidth, rect.height)),
258266
borderPaint,
259267
);
260-
currentY -= dashWidth + dashSpace;
268+
currentY += dashWidth + dashSpace;
261269
}
262270
} else {
263271
canvas.drawRect(rect, borderPaint);
@@ -282,40 +290,26 @@ class CanvasPainter extends CustomPainter {
282290

283291
@override
284292
bool shouldRepaint(CanvasPainter oldPainter) {
285-
// Only repaint if the data or selection changes significantly
286293
return oldPainter.userCursors != userCursors ||
287294
oldPainter.canvasObjects.length != canvasObjects.length ||
288295
oldPainter.currentlySelectedObjectId != currentlySelectedObjectId ||
289-
// Deep comparison of canvas objects is expensive, but necessary if text content changes frequently
296+
oldPainter.interactionMode != interactionMode || // UPDATED: Add interactionMode check
290297
_hasCanvasObjectsChanged(oldPainter.canvasObjects, canvasObjects);
291298
}
292299

293-
// Helper method for deep comparison of canvas objects
294300
bool _hasCanvasObjectsChanged(Map<String, CanvasObject> oldObjects, Map<String, CanvasObject> newObjects) {
295301
if (oldObjects.length != newObjects.length) return true;
296-
297302
for (final id in newObjects.keys) {
298303
final newObj = newObjects[id];
299304
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)
305+
if (oldObj == null || newObj == null) return true;
304306
if (newObj.id != oldObj.id) return true;
305-
306-
// Check basic properties
307-
if (newObj.color != oldObj.color ||
308-
newObj.getBounds() != oldObj.getBounds()) {
307+
if (newObj.color != oldObj.color || newObj.getBounds() != oldObj.getBounds()) {
309308
return true;
310309
}
311-
312-
// Check textDelta content
313310
if (newObj.textDelta != oldObj.textDelta) {
314311
return true;
315312
}
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.
319313
}
320314
return false;
321315
}

0 commit comments

Comments
 (0)