Skip to content

Commit 54a7189

Browse files
committed
feat(incomplete): text box feature
1 parent 364d7c9 commit 54a7189

20 files changed

+1096
-216
lines changed

lib/app.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:cookethflow/core/providers/supabase_provider.dart';
22
import 'package:cookethflow/core/router/app_route_config.dart';
33
import 'package:cookethflow/core/theme/app_theme.dart';
44
import 'package:flutter/material.dart';
5+
import 'package:flutter_quill/flutter_quill.dart';
56
import 'package:flutter_screenutil/flutter_screenutil.dart';
67
import 'package:provider/provider.dart';
78

@@ -22,6 +23,17 @@ class MyApp extends StatelessWidget {
2223
themeMode: themeProvider.isDark ? ThemeMode.dark : ThemeMode.light,
2324
debugShowCheckedModeBanner: false,
2425
routerConfig: AppRouteConfig.returnRouter(),
26+
localizationsDelegates: const [
27+
// GlobalMaterialLocalizations.delegate,
28+
// GlobalWidgetsLocalizations.delegate,
29+
// GlobalCupertinoLocalizations.delegate,
30+
FlutterQuillLocalizations.delegate, // REQUIRED for flutter_quill
31+
],
32+
// NEW: Define supported locales (at least English for now)
33+
supportedLocales: const [
34+
Locale('en', ''), // English
35+
// Add other locales your app supports if needed, e.g., Locale('es', '') for Spanish
36+
],
2537
);
2638
},
2739
),

lib/core/utils/enums.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
import 'package:phosphor_flutter/phosphor_flutter.dart';
23

34
enum ProviderState { inital, empty, loading, loaded, success, error }
45

@@ -31,7 +32,6 @@ enum ShapeType {
3132
invertedTriangle,
3233
}
3334

34-
3535
enum DrawMode {
3636
pointer(iconData: Icons.pan_tool_alt),
3737
circle(iconData: Icons.circle_outlined),
@@ -42,7 +42,8 @@ enum DrawMode {
4242
parallelogram(iconData: Icons.square_foot_outlined), // Placeholder
4343
cylinder(iconData: Icons.view_in_ar_outlined), // Placeholder
4444
triangle(iconData: Icons.change_history),
45-
invertedTriangle(iconData: Icons.warning_amber_rounded); // Placeholder
45+
invertedTriangle(iconData: Icons.warning_amber_rounded), // Placeholder
46+
textBox(iconData: PhosphorIconsRegular.textT);
4647

4748
const DrawMode({required this.iconData});
4849
final IconData iconData;
@@ -55,4 +56,5 @@ enum InteractionMode {
5556
resizingTopRight,
5657
resizingBottomLeft,
5758
resizingBottomRight,
58-
}
59+
editingText,
60+
}

lib/features/models/canvas_models/canvas_object.dart

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:cookethflow/features/models/canvas_models/objects/parallelogram_
88
import 'package:cookethflow/features/models/canvas_models/objects/rectangle_object.dart';
99
import 'package:cookethflow/features/models/canvas_models/objects/rounded_square_object.dart';
1010
import 'package:cookethflow/features/models/canvas_models/objects/square_object.dart';
11+
import 'package:cookethflow/features/models/canvas_models/objects/text_box_object.dart';
1112
import 'package:cookethflow/features/models/canvas_models/objects/triangle_object.dart';
1213
import 'package:cookethflow/features/models/canvas_models/synced_object.dart';
1314
import 'package:flutter/material.dart';
@@ -19,14 +20,17 @@ extension RandomColor on Color {
1920

2021
static Color getRandomFromUserId(String id) {
2122
final seed = utf8.encode(id).reduce((value, element) => value + element);
22-
return Color((Random(seed).nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
23+
return Color(
24+
(Random(seed).nextDouble() * 0xFFFFFF).toInt(),
25+
).withOpacity(1.0);
2326
}
2427
}
2528

2629
abstract class CanvasObject extends SyncedObject {
2730
final Color color;
31+
final String? textDelta;
2832

29-
CanvasObject({required super.id, required this.color});
33+
CanvasObject({required super.id, required this.color, this.textDelta});
3034

3135
factory CanvasObject.fromJson(Map<String, dynamic> json) {
3236
final objectType = json['object_type'];
@@ -49,14 +53,19 @@ abstract class CanvasObject extends SyncedObject {
4953
return Triangle.fromJson(json);
5054
case InvertedTriangle.type:
5155
return InvertedTriangle.fromJson(json);
56+
case TextBoxObject.type:
57+
return TextBoxObject.fromJson(json);
5258
default:
5359
throw UnimplementedError('Unknown object_type: $objectType');
5460
}
5561
}
5662

5763
bool intersectsWith(Offset point);
58-
CanvasObject copyWith();
64+
CanvasObject copyWith({String? textDelta});
5965
CanvasObject move(Offset delta);
60-
Rect getBounds(); // New method to get the bounding box
61-
CanvasObject resize(Offset newTopLeft, Offset newBottomRight); // New method for resizing
62-
}
66+
Rect getBounds();
67+
CanvasObject resize(
68+
Offset newTopLeft,
69+
Offset newBottomRight,
70+
);
71+
}

lib/features/models/canvas_models/canvas_painter.dart

Lines changed: 199 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
// lib/features/models/canvas_models/canvas_painter.dart
2+
13
import 'dart:math';
4+
import 'dart:ui';
5+
import 'dart:convert'; // Import for jsonDecode
6+
27
import 'package:cookethflow/features/models/canvas_models/canvas_object.dart';
38
import 'package:cookethflow/features/models/canvas_models/objects/circle_object.dart';
49
import 'package:cookethflow/features/models/canvas_models/objects/cylinder_object.dart';
@@ -8,9 +13,11 @@ import 'package:cookethflow/features/models/canvas_models/objects/parallelogram_
813
import 'package:cookethflow/features/models/canvas_models/objects/rectangle_object.dart';
914
import 'package:cookethflow/features/models/canvas_models/objects/rounded_square_object.dart';
1015
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
1117
import 'package:cookethflow/features/models/canvas_models/objects/triangle_object.dart';
1218
import 'package:cookethflow/features/models/canvas_models/user_cursor.dart';
1319
import 'package:flutter/material.dart';
20+
import 'package:flutter_quill/flutter_quill.dart'; // NEW: Import flutter_quill
1421

1522
class 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

Comments
 (0)