creek

The AI Image Editor of 2030

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}