Skip to content

Commit 0260ccb

Browse files
committed
Add flip horizontal and vertical to all elements, closes #1058
1 parent 33d92ad commit 0260ccb

6 files changed

Lines changed: 100 additions & 32 deletions

File tree

app/lib/dialogs/elements.dart

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ ContextMenuBuilder buildElementsContextMenu(
2626
) {
2727
final cubit = bloc.editorController;
2828
final settingsCubit = state.settingsCubit;
29-
final operations =
30-
Map<
31-
Renderer<PadElement>,
32-
Map<RendererOperation, RendererOperationCallback>
33-
>.fromIterable(renderers, value: (e) => e.getOperations());
34-
final operationKeys = operations.values.expand((e) => e.keys).toList();
29+
final operations = {
30+
for (final renderer in renderers) renderer: renderer.getOperations(),
31+
};
32+
final operationKeys = {
33+
...operations.values.expand((e) => e.keys),
34+
RendererOperation.flipHorizontal,
35+
RendererOperation.flipVertical,
36+
};
3537
return (context) {
3638
final rendererContextMenuItem = renderers.length == 1
3739
? renderers.first.getContextMenuItem(bloc, context)
@@ -142,26 +144,30 @@ ContextMenuBuilder buildElementsContextMenu(
142144
.toList(),
143145
label: AppLocalizations.of(context).arrange,
144146
),
145-
if (operationKeys.isNotEmpty)
146-
ContextMenuGroup(
147-
label: AppLocalizations.of(context).operations,
148-
icon: const PhosphorIcon(PhosphorIconsLight.wrench),
149-
children: operationKeys
150-
.map(
151-
(e) => MenuItemButton(
152-
leadingIcon: PhosphorIcon(e.icon(PhosphorIconsStyle.light)),
153-
child: Text(e.getLocalizedName(context)),
154-
onPressed: () {
147+
ContextMenuGroup(
148+
label: AppLocalizations.of(context).operations,
149+
icon: const PhosphorIcon(PhosphorIconsLight.wrench),
150+
children: operationKeys
151+
.map(
152+
(e) => MenuItemButton(
153+
leadingIcon: PhosphorIcon(e.icon(PhosphorIconsStyle.light)),
154+
child: Text(e.getLocalizedName(context)),
155+
onPressed: () {
156+
final flipAxis = e.flipAxis;
157+
if (flipAxis == null) {
155158
operations.values
156159
.map((v) => v[e])
157160
.nonNulls
158161
.forEach((e) => e(bloc, context));
159-
if (context.mounted) Navigator.of(context).pop(true);
160-
},
161-
),
162-
)
163-
.toList(),
164-
),
162+
} else {
163+
_flipElements(bloc, renderers, rect, flipAxis);
164+
}
165+
if (context.mounted) Navigator.of(context).pop(true);
166+
},
167+
),
168+
)
169+
.toList(),
170+
),
165171
?rendererContextMenuItem,
166172
if (renderers.length == 1 &&
167173
exportService.isExportable(renderers.first.element))
@@ -199,3 +205,24 @@ ContextMenuBuilder buildElementsContextMenu(
199205
];
200206
};
201207
}
208+
209+
void _flipElements(
210+
DocumentBloc bloc,
211+
List<Renderer<PadElement>> renderers,
212+
Rect selectionRect,
213+
Axis axis,
214+
) {
215+
final changes = Map.fromEntries(
216+
renderers.map((renderer) {
217+
final id = renderer.element.id;
218+
if (id == null) return null;
219+
final transformed = renderer.flip(
220+
axis: axis,
221+
selectionRect: selectionRect,
222+
);
223+
if (transformed == null) return null;
224+
return MapEntry(id, [transformed.element]);
225+
}).nonNulls,
226+
);
227+
if (changes.isNotEmpty) bloc.add(ElementsChanged(changes));
228+
}

app/lib/handlers/select.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,6 @@ class SelectHandler extends Handler<SelectTool> {
334334
);
335335
final hit = hits.firstOrNull;
336336
final rect = hit?.expandedRect;
337-
final selectionRect = getSelectionRect();
338337
final hitSelection = _isSelectionHit(
339338
position,
340339
context.getCameraTransform(),
@@ -358,7 +357,7 @@ class SelectHandler extends Handler<SelectTool> {
358357
context.getClipboardManager(),
359358
localPosition,
360359
_selected,
361-
selectionRect,
360+
getSelectionRect(),
362361
),
363362
);
364363
if (result ?? false) {

app/lib/renderers/elements/image.dart

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -232,14 +232,6 @@ class ImageRenderer extends Renderer<ImageElement> {
232232
updateImage(bloc, (cmd) => cmd.filter(updateImageBackground())),
233233
RendererOperation.grayscale: (bloc, context) =>
234234
updateImage(bloc, (cmd) => cmd.grayscale()),
235-
RendererOperation.flipHorizontal: (bloc, context) => updateImage(
236-
bloc,
237-
(cmd) => cmd.flip(direction: img.FlipDirection.horizontal),
238-
),
239-
RendererOperation.flipVertical: (bloc, context) => updateImage(
240-
bloc,
241-
(cmd) => cmd.flip(direction: img.FlipDirection.vertical),
242-
),
243235
};
244236
}
245237
}

app/lib/renderers/renderer.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,12 @@ enum RendererOperation {
474474
RendererOperation.flipHorizontal => PhosphorIcons.flipHorizontal,
475475
RendererOperation.flipVertical => PhosphorIcons.flipVertical,
476476
};
477+
478+
Axis? get flipAxis => switch (this) {
479+
RendererOperation.flipHorizontal => Axis.horizontal,
480+
RendererOperation.flipVertical => Axis.vertical,
481+
_ => null,
482+
};
477483
}
478484

479485
typedef RendererOperationCallback =
@@ -764,6 +770,27 @@ abstract class Renderer<T> {
764770
);
765771
}
766772

773+
Renderer<T>? flip({required Axis axis, required Rect selectionRect}) {
774+
final bounds = expandedRect ?? rect;
775+
if (bounds == null) return null;
776+
final offset = switch (axis) {
777+
Axis.horizontal => Offset(
778+
2 * (selectionRect.center.dx - bounds.center.dx),
779+
0,
780+
),
781+
Axis.vertical => Offset(
782+
0,
783+
2 * (selectionRect.center.dy - bounds.center.dy),
784+
),
785+
};
786+
return transform(
787+
position: offset,
788+
scaleX: axis == Axis.horizontal ? -1 : 1,
789+
scaleY: axis == Axis.vertical ? -1 : 1,
790+
positionIsBounds: true,
791+
);
792+
}
793+
767794
Renderer<T>? _transform({
768795
required Offset position,
769796
required double rotation,

app/test/renderers/shape_renderer_test.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'dart:ui';
44
import 'package:butterfly/renderers/renderer.dart';
55
import 'package:butterfly_api/butterfly_api.dart';
66
import 'package:flutter_test/flutter_test.dart';
7+
import 'package:flutter/widgets.dart' show Axis;
78

89
void main() {
910
group('Zero Size Shapes', () {
@@ -112,6 +113,27 @@ void main() {
112113
);
113114
});
114115

116+
test('flip mirrors the renderer inside the selection bounds', () {
117+
final renderer = ShapeRenderer(
118+
ShapeElement(
119+
firstPosition: const Point(120, 210),
120+
secondPosition: const Point(220, 260),
121+
),
122+
);
123+
124+
final horizontal = renderer.flip(
125+
axis: Axis.horizontal,
126+
selectionRect: const Rect.fromLTWH(100, 200, 200, 100),
127+
)!;
128+
final vertical = renderer.flip(
129+
axis: Axis.vertical,
130+
selectionRect: const Rect.fromLTWH(100, 200, 200, 100),
131+
)!;
132+
133+
expect(horizontal.expandedRect, const Rect.fromLTWH(180, 210, 100, 50));
134+
expect(vertical.expandedRect, const Rect.fromLTWH(120, 240, 100, 50));
135+
});
136+
115137
test('horizontal flip keeps a rotated element in the target bounds', () {
116138
final renderer = ShapeRenderer(
117139
ShapeElement(

metadata/en-US/changelogs/189.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Add custom fonts to label ([#1011](https://github.com/LinwoodDev/Butterfly/issues/1011))
55
* Add option to customize default file name globally and in template ([#1041](https://github.com/LinwoodDev/Butterfly/issues/1041))
66
* Reorder top corner menu to have home on top ([#1161](https://github.com/LinwoodDev/Butterfly/issues/1161))
7+
* Add flip horizontal and vertical to all elements ([#1058](https://github.com/LinwoodDev/Butterfly/issues/1058))
78
* Rebuild internal settings pages
89
* Add search bar to settings pages ([#1158](https://github.com/LinwoodDev/Butterfly/issues/1158))
910
* Always have settings value on the right side

0 commit comments

Comments
 (0)