Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 36 additions & 21 deletions lib/widgets/draggable_widgets/image/placed_image_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,39 +80,54 @@ class _PlacedImageBuilderState extends State<PlacedImageBuilder> {
pinnedScreenPosition = nextPinned;
}

/// Stores the new scale, and the position that keeps the image's top-left
/// where the resize pinned it. On defense the image hangs from its
/// bottom-right corner, so that position moves with the size.
/// Stores the new scale at once, so a page save right after release keeps
/// it, with the position that keeps the image's top-left where the resize
/// pinned it. On defense the image hangs from its bottom-right corner, so
/// that position moves with the size.
///
/// Waits for the frame that lays out the last drag update, so the stored
/// position matches the size the user let go at.
/// The last drag update may not be laid out yet, so the pin holds for one
/// more frame, then the position is stored again if it moved, unless a
/// page switch loaded another page's copy of the image, which keeps its id.
Future<void> _finishResize(WidgetRef ref) async {
final resizedImage = widget.placedImage;
final pinned = pinnedScreenPosition;
if (pinned != null) _storeResize(ref, resizedImage, pinned);
await WidgetsBinding.instance.endOfFrame;
if (!mounted) return;

final pinned = pinnedScreenPosition;
final renderBox = _boxKey.currentContext?.findRenderObject() as RenderBox?;
if (pinned != null && renderBox != null) {
final coordinateSystem = CoordinateSystem.instance;
final position = coordinateSystem.screenToCoordinate(
coordinateSystem.screenPositionFromSide(
sideScreenPosition: pinned,
reflectionOffset: renderBox.size.bottomRight(Offset.zero),
isAttack: widget.isAttack,
),
);
ref
.read(placedImageProvider.notifier)
.resize(widget.placedImage.id, scale: localScale!, position: position);
ref.read(strategyProvider.notifier).setUnsaved();
}
final stillLoaded = ref
.read(placedImageProvider)
.images
.any((image) => identical(image, resizedImage));
if (pinned != null && stillLoaded) _storeResize(ref, resizedImage, pinned);
setState(() {
isPanning = false;
pinnedScreenPosition = null;
pointerShift = 0;
});
}

/// Writes [image]'s new scale and the position that puts its top-left at
/// [pinned] as laid out now, unless both are already stored.
void _storeResize(WidgetRef ref, PlacedImage image, Offset pinned) {
final renderBox = _boxKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) return;

final coordinateSystem = CoordinateSystem.instance;
final position = coordinateSystem.screenToCoordinate(
coordinateSystem.screenPositionFromSide(
sideScreenPosition: pinned,
reflectionOffset: renderBox.size.bottomRight(Offset.zero),
isAttack: widget.isAttack,
),
);
if (image.scale == localScale && image.position == position) return;
ref
.read(placedImageProvider.notifier)
.resize(image.id, scale: localScale!, position: position);
ref.read(strategyProvider.notifier).setUnsaved();
}

@override
Widget build(BuildContext context) {
if (localScale == null) {
Expand Down
56 changes: 35 additions & 21 deletions lib/widgets/draggable_widgets/text/placed_text_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,32 +85,25 @@ class _PlacedTextBuilderState extends ConsumerState<PlacedTextBuilder> {
pinnedScreenPosition = nextPinned;
}

/// Stores the new width, and the position that keeps the box's top-left
/// where the resize pinned it. On defense the box hangs from its
/// bottom-right corner, so that position moves with the size.
/// Stores the new width at once, so a page save right after release keeps
/// it, with the position that keeps the box's top-left where the resize
/// pinned it. On defense the box hangs from its bottom-right corner, so
/// that position moves with the size.
///
/// Waits for the frame that lays out the last drag update, because the
/// text's height, and so its defense position, follows from its width.
/// The text's height, and so its defense position, follows from its width,
/// and the last drag update may not be laid out yet. So the pin holds for
/// one more frame, then the position is stored again if it moved, unless a
/// page switch loaded another page's copy of the text, which keeps its id.
Future<void> _finishResize() async {
final resizedText = widget.placedText;
final pinned = pinnedScreenPosition;
if (pinned != null) _storeResize(resizedText, pinned);
Comment thread
SunkenInTime marked this conversation as resolved.
await WidgetsBinding.instance.endOfFrame;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
if (!mounted) return;

final pinned = pinnedScreenPosition;
final renderBox = _boxKey.currentContext?.findRenderObject() as RenderBox?;
if (pinned != null && renderBox != null) {
final coordinateSystem = CoordinateSystem.instance;
final position = coordinateSystem.screenToCoordinate(
coordinateSystem.screenPositionFromSide(
sideScreenPosition: pinned,
reflectionOffset: renderBox.size.bottomRight(Offset.zero),
isAttack: widget.isAttack,
),
);
ref
.read(textProvider.notifier)
.resize(widget.placedText.id, size: localSize!, position: position);
ref.read(strategyProvider.notifier).setUnsaved();
}
final stillLoaded =
ref.read(textProvider).any((text) => identical(text, resizedText));
if (pinned != null && stillLoaded) _storeResize(resizedText, pinned);
setState(() {
isPanning = false;
isDragging = false;
Expand All @@ -119,6 +112,27 @@ class _PlacedTextBuilderState extends ConsumerState<PlacedTextBuilder> {
});
}

/// Writes [text]'s new width and the position that puts its top-left at
/// [pinned] as laid out now, unless both are already stored.
void _storeResize(PlacedText text, Offset pinned) {
final renderBox = _boxKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) return;

final coordinateSystem = CoordinateSystem.instance;
final position = coordinateSystem.screenToCoordinate(
coordinateSystem.screenPositionFromSide(
sideScreenPosition: pinned,
reflectionOffset: renderBox.size.bottomRight(Offset.zero),
isAttack: widget.isAttack,
),
);
if (text.size == localSize && text.position == position) return;
ref
.read(textProvider.notifier)
.resize(text.id, size: localSize!, position: position);
ref.read(strategyProvider.notifier).setUnsaved();
}

@override
Widget build(BuildContext context) {
final texts = ref.watch(textProvider);
Expand Down
102 changes: 102 additions & 0 deletions test/placed_box_resize_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,106 @@ void main() {
tester.getRect(box).topLeft, offsetMoreOrLessEquals(onDefense.topLeft));
expect(container.read(textProvider).single.size, greaterThan(200));
});

testWidgets('a release before the next frame keeps its last movement',
(tester) async {
await pumpBox(
tester,
seed: (container) => container.read(textProvider.notifier).fromHive([
PlacedText(
id: 'text-1',
position: originalPosition,
size: 200,
sizeVersion: PlacedText.currentSizeVersion,
)..text = 'Yo text boxes resize properly on both sides now',
]),
side: ValueNotifier(false),
builder: (ref, isAttack) {
final placedText = ref.watch(textProvider).single;
return PlacedTextBuilder(
key: ValueKey(placedText.id),
size: placedText.size,
placedText: placedText,
isAttack: isAttack,
onDragEnd: (_) {},
);
},
);
final box = find.byType(TextScaleController);
final handle = find.byWidgetPredicate(
(widget) =>
widget is MouseRegion &&
widget.cursor == SystemMouseCursors.resizeLeftRight,
);

final gesture = await tester.startGesture(tester.getCenter(handle));
for (var i = 0; i < 3; i++) {
await gesture.moveBy(const Offset(20, 0));
await tester.pump();
}
final shown = tester.getRect(box);

// A last move and the release arrive before the next frame draws it.
await gesture.moveBy(const Offset(40, 0));
await gesture.up();
await tester.pumpAndSettle();

expect(tester.getRect(box).topLeft, offsetMoreOrLessEquals(shown.topLeft));
expect(tester.getRect(box).width, closeTo(shown.width + 40, 1));
});

testWidgets('a page switch right after release keeps the resize on its page',
(tester) async {
final container = await pumpBox(
tester,
seed: (container) => container.read(textProvider.notifier).fromHive([
PlacedText(
id: 'text-1',
position: originalPosition,
size: 200,
sizeVersion: PlacedText.currentSizeVersion,
)..text = 'Yo text boxes resize properly on both sides now',
]),
side: ValueNotifier(false),
builder: (ref, isAttack) {
final placedText = ref.watch(textProvider).single;
return PlacedTextBuilder(
key: ValueKey(placedText.id),
size: placedText.size,
placedText: placedText,
isAttack: isAttack,
onDragEnd: (_) {},
);
},
);
final handle = find.byWidgetPredicate(
(widget) =>
widget is MouseRegion &&
widget.cursor == SystemMouseCursors.resizeLeftRight,
);

final gesture = await tester.startGesture(tester.getCenter(handle));
for (var i = 0; i < 3; i++) {
await gesture.moveBy(const Offset(20, 0));
await tester.pump();
}
final resized = container.read(textProvider).single;
await gesture.up();

// Before the frame, the page is saved and the next page loads its copy
// of the text. The save must already see the new width.
expect(resized.size, greaterThan(200));
final copy = PlacedText(
id: 'text-1',
position: const Offset(100, 100),
size: 200,
sizeVersion: PlacedText.currentSizeVersion,
)..text = 'Yo text boxes resize properly on both sides now';
container.read(textProvider.notifier).fromHive([copy]);
await tester.pumpAndSettle();

final loaded = container.read(textProvider).single;
expect(loaded.size, 200);
expect(loaded.position, const Offset(100, 100));
});
}
Loading