diff --git a/lib/widgets/editor/entry_editor_page.dart b/lib/widgets/editor/entry_editor_page.dart index a77a3a939..c8cba2b81 100644 --- a/lib/widgets/editor/entry_editor_page.dart +++ b/lib/widgets/editor/entry_editor_page.dart @@ -117,13 +117,13 @@ class _ImageEditorPageState extends State { ); } - void _onActionChanged() => _updateImageMargin(); - - void _updateImageMargin() { - if (_actionNotifier.value == EditorAction.transform) { - _marginNotifier.value = Cropper.imageMargin; - } else { - _marginNotifier.value = EdgeInsets.zero; + void _onActionChanged() { + switch(_actionNotifier.value) { + case EditorAction.transform: + _transformController.reset(); + _marginNotifier.value = Cropper.imageMargin; + default: + _marginNotifier.value = EdgeInsets.zero; } } diff --git a/lib/widgets/editor/transform/control_panel.dart b/lib/widgets/editor/transform/control_panel.dart index 1dc1ccbae..f025b7f92 100644 --- a/lib/widgets/editor/transform/control_panel.dart +++ b/lib/widgets/editor/transform/control_panel.dart @@ -97,7 +97,7 @@ class _TransformControlPanelState extends State with Tick OverlayButton( child: StreamBuilder( stream: transformController.transformationStream, - builder: (context, snapshot) { + builder: (context, _) { return IconButton( icon: const Icon(AIcons.apply), onPressed: transformController.modified ? () => widget.onApply(transformController.transformation) : null, @@ -122,17 +122,17 @@ class CropControlPanel extends StatelessWidget { @override Widget build(BuildContext context) { - final aspectRatioNotifier = context.select>((v) => v.aspectRatioNotifier); + final controller = context.watch(); return ListView.builder( scrollDirection: Axis.horizontal, shrinkWrap: true, itemBuilder: (context, index) { final ratio = CropAspectRatio.values[index]; - void setAspectRatio() => aspectRatioNotifier.value = ratio; + void setAspectRatio() => controller.setAspectRatio(ratio); return CaptionedButton( iconButtonBuilder: (context, focusNode) { return ValueListenableBuilder( - valueListenable: aspectRatioNotifier, + valueListenable: controller.aspectRatioNotifier, builder: (context, selectedRatio, child) { return IconButton( color: ratio == selectedRatio ? Theme.of(context).colorScheme.primary : null, @@ -166,17 +166,21 @@ class RotationControlPanel extends StatelessWidget { Expanded( child: StreamBuilder( stream: controller.transformationStream, - builder: (context, snapshot) { - final transformation = snapshot.data ?? Transformation.zero; - return Slider( - value: transformation.straightenDegrees, - min: TransformController.straightenDegreesMin, - max: TransformController.straightenDegreesMax, - divisions: 18, - onChangeStart: (v) => controller.activity = TransformActivity.straighten, - onChangeEnd: (v) => controller.activity = TransformActivity.none, - label: angleFormatter.format(transformation.straightenDegrees), - onChanged: (v) => controller.straightenDegrees = v, + builder: (context, _) { + final degrees = controller.transformation.straightenDegrees; + return SliderTheme( + data: SliderTheme.of(context).copyWith( + showValueIndicator: ShowValueIndicator.always, + ), + child: Slider( + value: degrees, + min: TransformController.straightenDegreesMin, + max: TransformController.straightenDegreesMax, + onChangeStart: (v) => controller.activity = TransformActivity.straighten, + onChangeEnd: (v) => controller.activity = TransformActivity.none, + label: angleFormatter.format(degrees), + onChanged: (v) => controller.straightenDegrees = v, + ), ); }, ), diff --git a/lib/widgets/editor/transform/controller.dart b/lib/widgets/editor/transform/controller.dart index 481a86269..57328ca0e 100644 --- a/lib/widgets/editor/transform/controller.dart +++ b/lib/widgets/editor/transform/controller.dart @@ -57,6 +57,7 @@ class TransformController { region: CropRegion.fromRect(Offset.zero & displaySize), ); _transformationStreamController.add(_transformation); + setAspectRatio(CropAspectRatio.free); } void flipHorizontally() { @@ -93,6 +94,8 @@ class TransformController { _activityStreamController.add(_activity); } + void setAspectRatio(CropAspectRatio ratio) => aspectRatioNotifier.value = ratio; + void _onAspectRatioChanged() { // TODO TLAD [crop] apply } diff --git a/lib/widgets/editor/transform/cropper.dart b/lib/widgets/editor/transform/cropper.dart index 184fcc452..f4538970e 100644 --- a/lib/widgets/editor/transform/cropper.dart +++ b/lib/widgets/editor/transform/cropper.dart @@ -437,20 +437,12 @@ class _CropperState extends State with SingleTickerProviderStateMixin { } void _onViewStateChanged(MagnifierState state) { - final currentOutline = _outlineNotifier.value; - - // TODO TLAD [crop] use other strat - - // switch (state.source) { - // case ChangeSource.internal: - // case ChangeSource.animation: - // case ChangeSource.gesture: - // } - switch (transformController.activity) { case TransformActivity.none: + break; case TransformActivity.straighten: case TransformActivity.pan: + final currentOutline = _outlineNotifier.value; _setOutline(_applyCropRatioToOutline(currentOutline, _RatioStrategy.contain)); case TransformActivity.resize: break; @@ -488,8 +480,26 @@ class _CropperState extends State with SingleTickerProviderStateMixin { if (targetOutline.isEmpty || viewState == null || viewportSize == null) return; // ensure outline is within content - final targetRegion = _regionFromOutline(viewState, targetOutline); + var targetRegion = _regionFromOutline(viewState, targetOutline); var newOutline = _containedOutlineFromRegion(viewState, targetRegion); + final outlineWidthDelta = targetOutline.width - newOutline.width; + final outlineHeightDelta = targetOutline.height - newOutline.height; + if (outlineWidthDelta > precisionErrorTolerance || outlineHeightDelta > precisionErrorTolerance) { + // keep outline area if possible, otherwise trim + final regionToOutlineMatrix = _getRegionToOutlineMatrix(viewState); + final rect = Offset.zero & viewState.contentSize!; + final edgeRegionCorners = targetRegion.corners.where((v) => v.dx == rect.left || v.dx == rect.right || v.dy == rect.top || v.dy == rect.bottom).toSet(); + final edgeOutlineCorners = edgeRegionCorners.map(regionToOutlineMatrix.transformOffset).toSet(); + if (edgeOutlineCorners.isNotEmpty) { + final direction = edgeOutlineCorners.map((v) => newOutline.center - v).reduce((prev, v) => prev + v); + final movedOutline = targetOutline.shift(Offset( + outlineWidthDelta * direction.dx.sign, + outlineHeightDelta * direction.dy.sign, + )); + targetRegion = _regionFromOutline(viewState, movedOutline); + newOutline = _containedOutlineFromRegion(viewState, targetRegion); + } + } // ensure outline is large enough to be handled newOutline = Rect.fromLTWH( @@ -499,25 +509,13 @@ class _CropperState extends State with SingleTickerProviderStateMixin { max(newOutline.height, minDimension), ); - // ensure outline is within viewport - newOutline = Rect.fromLTRB( - max(newOutline.left, 0), - max(newOutline.top, 0), - min(newOutline.right, viewportSize.width), - min(newOutline.bottom, viewportSize.height), - ); - - final oldOutline = _outlineNotifier.value; _outlineNotifier.value = newOutline; switch (transformController.activity) { - case TransformActivity.none: - if (oldOutline.isEmpty) { - _updateCropRegion(); - } case TransformActivity.pan: case TransformActivity.resize: _updateCropRegion(); break; + case TransformActivity.none: case TransformActivity.straighten: break; }