Skip to content

Commit dbfbaa4

Browse files
Antara Paulclaude
authored andcommitted
feat: orthogonal connectors, scroll-to-pan, UI polish
- Orthogonal right-angle connector routing with rounded corners - Scroll-to-pan canvas navigation (replaces hand tool) - Auto contrast text on colored nodes - Default "Node X" labels on new shapes - Vertically centered text in nodes - Editable zoom percentage input - Sticky notes with folded corner design - Inline text editor without floating toolbar - Export dialog positioned below button - Consistent 16px rounded corners on all shapes - Smarter connector snap targeting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4134f76 commit dbfbaa4

21 files changed

+1507
-882
lines changed

lib/core/utils/enums.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ enum DrawMode {
5757
enum InteractionMode {
5858
none,
5959
moving,
60+
panning,
6061
resizingTopLeft,
6162
resizingTopRight,
6263
resizingBottomLeft,

lib/features/models/canvas_models/canvas_painter.dart

Lines changed: 252 additions & 52 deletions
Large diffs are not rendered by default.

lib/features/models/canvas_models/objects/rounded_square_object.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class RoundedSquare extends CanvasObject {
1616
required super.color,
1717
required this.topLeft,
1818
required this.bottomRight,
19-
this.cornerRadius = 10.0,
19+
this.cornerRadius = 16.0,
2020
super.textDelta,
2121
});
2222

@@ -26,7 +26,7 @@ class RoundedSquare extends CanvasObject {
2626
json['bottom_right']['y'],
2727
),
2828
topLeft = Offset(json['top_left']['x'], json['top_left']['y']),
29-
cornerRadius = json['corner_radius'] ?? 10.0,
29+
cornerRadius = json['corner_radius'] ?? 16.0,
3030
super(
3131
id: json['id'],
3232
color: Color(json['color'] as int),
@@ -36,7 +36,7 @@ class RoundedSquare extends CanvasObject {
3636
RoundedSquare.createNew(Offset defaultTopLeft, Offset defaultBottomRight)
3737
: topLeft = defaultTopLeft,
3838
bottomRight = defaultBottomRight,
39-
cornerRadius = 10.0,
39+
cornerRadius = 16.0,
4040
super(
4141
color: RandomColor.getRandom(),
4242
id: const Uuid().v4(),

lib/features/workspace/pages/canvas_page.dart

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'package:cookethflow/core/providers/supabase_provider.dart';
22
import 'package:cookethflow/core/utils/enums.dart';
33
import 'package:cookethflow/features/models/canvas_models/canvas_painter.dart';
4-
import 'package:cookethflow/features/models/workspace_model.dart';
54
import 'package:cookethflow/features/workspace/providers/canvas_provider.dart';
65
import 'package:cookethflow/features/workspace/providers/workspace_provider.dart';
76
import 'package:flutter/gestures.dart';
@@ -16,7 +15,6 @@ class CanvasPage extends StatelessWidget {
1615
Widget build(BuildContext context) {
1716
return Consumer3<WorkspaceProvider, CanvasProvider, SupabaseService>(
1817
builder: (context, workspaceProvider, canvasProvider, suprovider, child) {
19-
final isHandToolActive = workspaceProvider.currentMode == DrawMode.hand;
2018
return Scaffold(
2119
backgroundColor: workspaceProvider.currentWorkspaceColor,
2220
body: Listener(
@@ -27,11 +25,16 @@ class CanvasPage extends StatelessWidget {
2725
// double-click logic could be implemented here if needed
2826
}
2927
},
28+
onPointerSignal: (event) {
29+
if (event is PointerScrollEvent) {
30+
// Scroll to pan (like Figma)
31+
final matrix = canvasProvider.transformationController.value.clone();
32+
matrix.translate(-event.scrollDelta.dx, -event.scrollDelta.dy);
33+
canvasProvider.transformationController.value = matrix;
34+
}
35+
},
3036
child: MouseRegion(
31-
cursor:
32-
isHandToolActive
33-
? SystemMouseCursors.grab
34-
: SystemMouseCursors.basic,
37+
cursor: SystemMouseCursors.basic,
3538
onHover: (event) {
3639
final Matrix4 transform =
3740
canvasProvider.transformationController.value;
@@ -64,38 +67,29 @@ class CanvasPage extends StatelessWidget {
6467
maxScale: 4.0,
6568
boundaryMargin: const EdgeInsets.all(double.infinity),
6669
constrained: false,
67-
// Enable panning only when Hand Tool is active
68-
panEnabled: isHandToolActive,
70+
panEnabled: false,
6971
scaleEnabled:
7072
workspaceProvider.interactionMode !=
7173
InteractionMode.editingText,
7274
child: Container(
7375
color: workspaceProvider.currentWorkspaceColor,
7476
child: GestureDetector(
75-
// Disable GestureDetector's pan events when Hand Tool is active
76-
onPanDown:
77-
isHandToolActive
78-
? null
79-
: (details) {
80-
workspaceProvider.onPanDown(
81-
DragDownDetails(
82-
globalPosition: details.localPosition,
83-
),
84-
);
85-
},
86-
onPanUpdate:
87-
isHandToolActive
88-
? null
89-
: (details) {
90-
workspaceProvider.onPanUpdate(
91-
DragUpdateDetails(
92-
globalPosition: details.localPosition,
93-
delta: details.delta,
94-
),
95-
);
96-
},
97-
onPanEnd:
98-
isHandToolActive ? null : workspaceProvider.onPanEnd,
77+
onPanDown: (details) {
78+
workspaceProvider.onPanDown(
79+
DragDownDetails(
80+
globalPosition: details.localPosition,
81+
),
82+
);
83+
},
84+
onPanUpdate: (details) {
85+
workspaceProvider.onPanUpdate(
86+
DragUpdateDetails(
87+
globalPosition: details.localPosition,
88+
delta: details.delta,
89+
),
90+
);
91+
},
92+
onPanEnd: workspaceProvider.onPanEnd,
9993
child: CustomPaint(
10094
size: const Size(20000, 20000),
10195
painter: CanvasPainter(

lib/features/workspace/pages/desktop/workspace_desktop.dart

Lines changed: 85 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -128,140 +128,99 @@ class _WorkspaceDesktopState extends State<WorkspaceDesktop> {
128128
onTap: () {
129129
_focusNode.requestFocus();
130130
},
131-
child: Padding(
132-
padding: EdgeInsets.symmetric(
133-
horizontal: 40.w,
134-
vertical: 40.h,
135-
),
136-
child: Stack(
131+
child: Stack(
137132
clipBehavior: Clip.none,
138133
children: [
139-
const CanvasPage(),
140-
const WorkspaceDrawer(),
141-
SizedBox(width: 20.w),
142-
Positioned(
143-
top: 0,
144-
left: 0.21.sw,
145-
child: UndoRedoButton(su: suprovider),
146-
),
147-
Positioned(
148-
top: 0,
149-
right: 0.001.sw,
150-
child: ExportProjectButton(
151-
su: suprovider,
152-
wp: provider,
153-
),
154-
),
155-
Positioned(right: 0, top: 0.10.sh, child: ToolBar()),
156-
Positioned(
157-
bottom: 0.h,
158-
right: 0.w,
159-
child: ZoomControlButton(),
160-
),
161-
Consumer2<WorkspaceProvider, CanvasProvider>(
162-
builder: (
163-
context,
164-
workspaceProvider,
165-
canvasProvider,
166-
child,
167-
) {
168-
return ListenableBuilder(
169-
listenable:
170-
canvasProvider.transformationController,
171-
builder: (context, child) {
172-
if (workspaceProvider.shouldShowObjectToolbox) {
173-
final selectedObject =
174-
workspaceProvider
175-
.canvasObjects[workspaceProvider
176-
.currentlySelectedObjectId!]!;
177-
178-
final matrix =
179-
canvasProvider
180-
.transformationController
181-
.value;
182-
Offset screenPosition;
183-
if (selectedObject is ConnectorObject) {
184-
// For connectors, position at midpoint of the line
185-
final source =
186-
workspaceProvider
187-
.canvasObjects[selectedObject
188-
.sourceId];
189-
final target =
190-
workspaceProvider
191-
.canvasObjects[selectedObject
192-
.targetId];
193-
194-
if (source != null && target != null) {
195-
final startPoint = source
196-
.getConnectionPoint(
197-
selectedObject.sourceAlignment,
198-
);
199-
final endPoint = target
200-
.getConnectionPoint(
201-
selectedObject.targetAlignment,
202-
);
203-
204-
// Calculate midpoint
205-
final midPoint = Offset(
206-
(startPoint.dx + endPoint.dx) / 2,
207-
(startPoint.dy + endPoint.dy) / 2,
208-
);
209-
210-
// Transform to screen coordinates
211-
final transformedMidPoint = matrix
212-
.transform3(
213-
vector_math.Vector3(
214-
midPoint.dx,
215-
midPoint.dy,
216-
0,
134+
// Canvas fills entire area, no padding
135+
const Positioned.fill(child: CanvasPage()),
136+
// UI overlay with 24px inset
137+
Positioned.fill(
138+
child: Padding(
139+
padding: const EdgeInsets.all(24),
140+
child: Stack(
141+
clipBehavior: Clip.none,
142+
children: [
143+
const WorkspaceDrawer(),
144+
Positioned(
145+
top: 0,
146+
left: 0.21.sw,
147+
child: UndoRedoButton(su: suprovider),
148+
),
149+
Positioned(
150+
top: 0,
151+
right: 0,
152+
child: ExportProjectButton(
153+
su: suprovider,
154+
wp: provider,
155+
),
156+
),
157+
Positioned(
158+
right: 0,
159+
top: 0,
160+
bottom: 0,
161+
child: Center(child: ToolBar()),
162+
),
163+
Positioned(
164+
bottom: 0,
165+
right: 0,
166+
child: ZoomControlButton(),
167+
),
168+
Consumer2<WorkspaceProvider, CanvasProvider>(
169+
builder: (context, workspaceProvider, canvasProvider, child) {
170+
return ListenableBuilder(
171+
listenable: canvasProvider.transformationController,
172+
builder: (context, child) {
173+
if (workspaceProvider.shouldShowObjectToolbox) {
174+
final selectedObject = workspaceProvider
175+
.canvasObjects[workspaceProvider.currentlySelectedObjectId!]!;
176+
final matrix = canvasProvider.transformationController.value;
177+
Offset screenPosition;
178+
if (selectedObject is ConnectorObject) {
179+
final source = workspaceProvider.canvasObjects[selectedObject.sourceId];
180+
final target = workspaceProvider.canvasObjects[selectedObject.targetId];
181+
if (source != null && target != null) {
182+
final startPoint = source.getConnectionPoint(selectedObject.sourceAlignment);
183+
final endPoint = target.getConnectionPoint(selectedObject.targetAlignment);
184+
final midPoint = Offset(
185+
(startPoint.dx + endPoint.dx) / 2,
186+
(startPoint.dy + endPoint.dy) / 2,
187+
);
188+
final transformedMidPoint = matrix.transform3(
189+
vector_math.Vector3(midPoint.dx, midPoint.dy, 0),
190+
);
191+
screenPosition = Offset(transformedMidPoint.x, transformedMidPoint.y);
192+
} else {
193+
return const SizedBox.shrink();
194+
}
195+
} else {
196+
final objectBounds = selectedObject.getBounds();
197+
final transformedTopCenter = matrix.transform3(
198+
vector_math.Vector3(objectBounds.topCenter.dx, objectBounds.topCenter.dy, 0),
199+
);
200+
screenPosition = Offset(transformedTopCenter.x, transformedTopCenter.y);
201+
}
202+
const double toolboxHeight = 48;
203+
return Positioned(
204+
left: screenPosition.dx,
205+
top: screenPosition.dy - toolboxHeight - 15,
206+
child: const FractionalTranslation(
207+
translation: Offset(-0.5, 0),
208+
child: NodeEditingToolbox(),
217209
),
218210
);
219-
220-
screenPosition = Offset(
221-
transformedMidPoint.x,
222-
transformedMidPoint.y,
223-
);
224-
} else {
225-
return const SizedBox.shrink();
226-
}
227-
} else {
228-
// For shapes, use the top center as before
229-
final objectBounds =
230-
selectedObject.getBounds();
231-
final transformedTopCenter = matrix
232-
.transform3(
233-
vector_math.Vector3(
234-
objectBounds.topCenter.dx,
235-
objectBounds.topCenter.dy,
236-
0,
237-
),
238-
);
239-
240-
screenPosition = Offset(
241-
transformedTopCenter.x,
242-
transformedTopCenter.y,
211+
}
212+
return const SizedBox.shrink();
213+
},
243214
);
244-
}
245-
246-
const double toolboxWidth = 240;
247-
const double toolboxHeight = 48;
248-
249-
return Positioned(
250-
left:
251-
screenPosition.dx - (toolboxWidth / 2),
252-
top: screenPosition.dy - toolboxHeight - 15,
253-
child: const NodeEditingToolbox(),
254-
);
255-
}
256-
return const SizedBox.shrink();
257-
},
258-
);
259-
},
215+
},
216+
),
217+
const ObjectTextEditor(),
218+
],
219+
),
220+
),
260221
),
261-
const ObjectTextEditor(),
262222
],
263223
),
264-
),
265224
),
266225
),
267226
),

lib/features/workspace/pages/mobile/workspace_mobile.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,15 @@ class _WorkspaceMobileState extends State<WorkspaceMobile> {
230230
);
231231
}
232232

233-
const double toolboxWidth = 240;
234233
const double toolboxHeight = 48;
235234

236235
return Positioned(
237-
left: screenPosition.dx - (toolboxWidth / 2),
236+
left: screenPosition.dx,
238237
top: screenPosition.dy - toolboxHeight - 15,
239-
child: const NodeEditingToolbox(),
238+
child: const FractionalTranslation(
239+
translation: Offset(-0.5, 0),
240+
child: NodeEditingToolbox(),
241+
),
240242
);
241243
}
242244
return const SizedBox.shrink();

lib/features/workspace/pages/tablet/workspace_tablet.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,13 +298,15 @@ class _WorkspaceTabletState extends State<WorkspaceTablet> {
298298
);
299299
}
300300

301-
const double toolboxWidth = 240;
302301
const double toolboxHeight = 48;
303302

304303
return Positioned(
305-
left: screenPosition.dx - (toolboxWidth / 2),
304+
left: screenPosition.dx,
306305
top: screenPosition.dy - toolboxHeight - 15,
307-
child: const NodeEditingToolbox(),
306+
child: const FractionalTranslation(
307+
translation: Offset(-0.5, 0),
308+
child: NodeEditingToolbox(),
309+
),
308310
);
309311
}
310312
return const SizedBox.shrink();

0 commit comments

Comments
 (0)