creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/ui/widgets/canvas/manipulating_box.dart (11851B)
1import 'dart:io';
2import 'dart:math' as math;
3import 'package:flutter/material.dart';
4import 'package:creekui/ui/styles/variables.dart';
5
6class ManipulatingBox extends StatefulWidget {
7 final String id;
8 final Offset position;
9 final Size size;
10 final double rotation;
11 final String type;
12 final String content;
13 final Map<String, dynamic> styleData;
14 final bool isSelected;
15 final bool isEditing;
16 final bool isFlashing;
17 final double viewScale;
18 final TransformationController transformationController;
19 final VoidCallback onTap;
20 final VoidCallback onDoubleTap;
21 final VoidCallback onDragStart;
22 final Function(Offset, Size, double) onUpdate;
23 final Function(Offset, Size, double) onDragEnd;
24 final TextEditingController? textController;
25 final FocusNode? focusNode;
26
27 const ManipulatingBox({
28 Key? key,
29 required this.id,
30 required this.position,
31 required this.size,
32 required this.rotation,
33 required this.type,
34 required this.content,
35 required this.styleData,
36 required this.isSelected,
37 required this.isEditing,
38 this.isFlashing = false,
39 required this.viewScale,
40 required this.transformationController,
41 required this.onTap,
42 required this.onDoubleTap,
43 required this.onDragStart,
44 required this.onUpdate,
45 required this.onDragEnd,
46 this.textController,
47 this.focusNode,
48 }) : super(key: key);
49
50 @override
51 State<ManipulatingBox> createState() => _ManipulatingBoxState();
52}
53
54class _ManipulatingBoxState extends State<ManipulatingBox> {
55 late Offset _pos;
56 late Size _size;
57 late double _rot;
58
59 // Gesture state
60 double _initialRotation = 0.0;
61 double _initialScale = 1.0;
62 bool _isTwoFingerGesture = false;
63 Offset _previousFocalPoint = Offset.zero;
64
65 @override
66 void initState() {
67 super.initState();
68 _updateInternalState();
69 }
70
71 @override
72 void didUpdateWidget(ManipulatingBox oldWidget) {
73 super.didUpdateWidget(oldWidget);
74 if (widget.position != oldWidget.position ||
75 widget.size != oldWidget.size ||
76 widget.rotation != oldWidget.rotation) {
77 _updateInternalState();
78 }
79 }
80
81 void _updateInternalState() {
82 _pos = widget.position;
83 _size = widget.size;
84 _rot = widget.rotation;
85 }
86
87 @override
88 Widget build(BuildContext context) {
89 return ValueListenableBuilder(
90 valueListenable: widget.transformationController,
91 builder: (context, matrix, child) {
92 final double zoom = matrix.getMaxScaleOnAxis();
93 final double handleScale = (1 / zoom).clamp(0.2, 5.0);
94 final double edgeThickness = 18 * handleScale;
95
96 return Positioned(
97 left: _pos.dx,
98 top: _pos.dy,
99 child: Transform.rotate(
100 angle: _rot,
101 child: Stack(
102 clipBehavior: Clip.none,
103 children: [
104 GestureDetector(
105 behavior: HitTestBehavior.opaque,
106 onTap: widget.onTap,
107 onDoubleTap: widget.onDoubleTap,
108 // Handle both single-finger drag and two-finger rotate/zoom using scale gestures
109 onScaleStart: (details) {
110 if (widget.isSelected && !widget.isEditing) {
111 _previousFocalPoint = details.focalPoint;
112 if (details.pointerCount == 2) {
113 // Two-finger gesture: rotate and zoom
114 _isTwoFingerGesture = true;
115 _initialRotation = _rot;
116 _initialScale = _size.width * _size.height;
117 } else {
118 // Single-finger gesture: prepare for drag
119 _isTwoFingerGesture = false;
120 }
121 widget.onDragStart();
122 }
123 },
124 onScaleUpdate: (details) {
125 if (widget.isSelected && !widget.isEditing) {
126 if (_isTwoFingerGesture && details.pointerCount == 2) {
127 // Two-finger: handle rotation and zoom
128 final newRotation = _initialRotation + details.rotation;
129
130 // Handle scale (zoom) - maintain aspect ratio
131 final scaleFactor = details.scale;
132 final newArea =
133 _initialScale * scaleFactor * scaleFactor;
134 final aspectRatio = _size.width / _size.height;
135 final newWidth = math
136 .sqrt(newArea * aspectRatio)
137 .clamp(20.0, 5000.0);
138 final newHeight = newWidth / aspectRatio;
139
140 setState(() {
141 _rot = newRotation % (2 * math.pi);
142 _size = Size(newWidth, newHeight);
143 });
144
145 widget.onUpdate(_pos, _size, _rot);
146 } else if (!_isTwoFingerGesture &&
147 details.pointerCount == 1) {
148 // Single-finger: handle drag using incremental focal point delta
149 final currentFocalPoint = details.focalPoint;
150 final delta = currentFocalPoint - _previousFocalPoint;
151 // Convert to canvas coordinates by dividing by zoom
152 final zoom = widget.viewScale;
153 final scaledDelta = delta / zoom;
154 // Rotate delta to account for element rotation
155 final rotated = _rotateVector(scaledDelta, -_rot);
156 setState(() {
157 _pos += scaledDelta;
158 _previousFocalPoint = currentFocalPoint;
159 });
160 widget.onUpdate(_pos, _size, _rot);
161 }
162 }
163 },
164 onScaleEnd: (details) {
165 if (widget.isSelected && !widget.isEditing) {
166 _isTwoFingerGesture = false;
167 widget.onDragEnd(_pos, _size, _rot);
168 }
169 },
170 child: Container(
171 width: _size.width,
172 height: _size.height,
173 decoration: BoxDecoration(
174 border:
175 widget.isFlashing
176 ? Border.all(
177 color: Colors.greenAccent,
178 width: 4 * handleScale,
179 )
180 : (widget.isSelected
181 ? Border.all(
182 color: Variables.selectionBorder,
183 width: 2 * handleScale,
184 )
185 : null),
186 ),
187 child:
188 widget.type == "file_image"
189 ? Image.file(
190 File(widget.content),
191 fit: BoxFit.contain,
192 )
193 : _buildText(),
194 ),
195 ),
196
197 if (widget.isSelected &&
198 !widget.isEditing &&
199 !widget.isFlashing) ...[
200 _buildEdgeHandle(
201 top: 0,
202 bottom: 0,
203 right: -edgeThickness / 2,
204 width: edgeThickness,
205 onUpdate:
206 (d) => setState(
207 () =>
208 _size = Size(
209 _size.width + d.delta.dx,
210 _size.height,
211 ),
212 ),
213 ),
214 _buildEdgeHandle(
215 top: 0,
216 bottom: 0,
217 left: -edgeThickness / 2,
218 width: edgeThickness,
219 onUpdate: (d) {
220 setState(() {
221 _pos += Offset(d.delta.dx, 0);
222 _size = Size(_size.width - d.delta.dx, _size.height);
223 });
224 },
225 ),
226 _buildEdgeHandle(
227 left: 0,
228 right: 0,
229 top: -edgeThickness / 2,
230 height: edgeThickness,
231 onUpdate: (d) {
232 setState(() {
233 _pos += Offset(0, d.delta.dy);
234 _size = Size(_size.width, _size.height - d.delta.dy);
235 });
236 },
237 ),
238 _buildEdgeHandle(
239 left: 0,
240 right: 0,
241 bottom: -edgeThickness / 2,
242 height: edgeThickness,
243 onUpdate:
244 (d) => setState(
245 () =>
246 _size = Size(
247 _size.width,
248 _size.height + d.delta.dy,
249 ),
250 ),
251 ),
252 ],
253 ],
254 ),
255 ),
256 );
257 },
258 );
259 }
260
261 Widget _buildEdgeHandle({
262 double? top,
263 double? bottom,
264 double? left,
265 double? right,
266 double? width,
267 double? height,
268 required Function(DragUpdateDetails) onUpdate,
269 }) {
270 return Positioned(
271 top: top,
272 bottom: bottom,
273 left: left,
274 right: right,
275 child: GestureDetector(
276 behavior: HitTestBehavior.translucent,
277 onPanUpdate: (d) {
278 onUpdate(d);
279 widget.onUpdate(_pos, _size, _rot);
280 },
281 onPanStart: (_) => widget.onDragStart(),
282 onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot),
283 child: Container(
284 width: width,
285 height: height,
286 color: Colors.transparent,
287 ),
288 ),
289 );
290 }
291
292 Widget _buildText() {
293 final style = TextStyle(
294 fontSize: (widget.styleData['style_fontSize'] ?? 24.0) as double,
295 color: Color(
296 widget.styleData['style_color'] ?? Variables.textPrimary.value,
297 ),
298 fontFamily: 'GeneralSans',
299 );
300
301 if (widget.isEditing) {
302 return Center(
303 child: IntrinsicWidth(
304 child: TextField(
305 controller: widget.textController,
306 focusNode: widget.focusNode,
307 autofocus: true,
308 maxLines: null,
309 textAlign: TextAlign.center,
310 style: style,
311 decoration: const InputDecoration(
312 border: InputBorder.none,
313 isDense: true,
314 contentPadding: EdgeInsets.zero,
315 ),
316 onChanged: (text) {
317 final span = TextSpan(text: text, style: style);
318 final tp = TextPainter(
319 text: span,
320 textDirection: TextDirection.ltr,
321 );
322 tp.layout(maxWidth: 10000);
323 setState(() => _size = Size(tp.width + 40, tp.height + 40));
324 widget.onUpdate(_pos, _size, _rot);
325 },
326 ),
327 ),
328 );
329 }
330 return Center(
331 child: Padding(
332 padding: const EdgeInsets.all(8.0),
333 child: Text(widget.content, textAlign: TextAlign.center, style: style),
334 ),
335 );
336 }
337
338 Offset _rotateVector(Offset vector, double angle) {
339 final cosA = math.cos(angle);
340 final sinA = math.sin(angle);
341 return Offset(
342 vector.dx * cosA - vector.dy * sinA,
343 vector.dx * sinA + vector.dy * cosA,
344 );
345 }
346}