Skip to content

Commit

Permalink
flexible popup positioning
Browse files Browse the repository at this point in the history
  • Loading branch information
drooxie committed May 23, 2023
1 parent 163af9f commit 04ceb89
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 130 deletions.
5 changes: 1 addition & 4 deletions example/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,11 @@ class _AppState extends State<App> {

@override
Widget build(BuildContext context) {
const _popupWidth = 250.0;
const _popupHeight = 150.0;

return Material(
// column here for test purposes;
// change mainAxisAlignment to test popup behaviour
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: [
LanguageToolTextField(
style: const TextStyle(),
Expand Down
2 changes: 1 addition & 1 deletion lib/domain/typedefs.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:flutter/widgets.dart';
import 'package:languagetool_textfield/core/controllers/colored_text_editing_controller.dart';
import 'package:languagetool_textfield/domain/mistake.dart';
import 'package:languagetool_textfield/utils/mistake_popup.dart';
import 'package:languagetool_textfield/utils/popup_overlay_renderer.dart';

/// Callback used to build popup body
typedef MistakeBuilderCallback = Widget Function(
Expand Down
1 change: 1 addition & 0 deletions lib/languagetool_textfield.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export 'implementations/lang_tool_service.dart';
export 'implementations/throttling_lang_tool_service.dart';
export 'presentation/language_tool_text_field.dart';
export 'utils/mistake_popup.dart';
export 'utils/popup_overlay_renderer.dart';
167 changes: 42 additions & 125 deletions lib/utils/mistake_popup.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:languagetool_textfield/domain/typedefs.dart';
import 'package:languagetool_textfield/languagetool_textfield.dart';
Expand Down Expand Up @@ -40,92 +38,6 @@ class MistakePopup {
}
}

/// Renderer used to show popup window overlay
class PopupOverlayRenderer {
OverlayEntry? _overlayEntry;

/// [PopupOverlayRenderer] constructor
PopupOverlayRenderer();

/// Render overlay entry on the screen with dismiss logic
OverlayEntry render(
BuildContext context, {
required Offset position,
required WidgetBuilder popupBuilder,
}) {
final Offset _popupPosition = _calculatePosition(context, position);

final _createdEntry = OverlayEntry(
builder: (context) => GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: dismiss,
child: Material(
color: Colors.transparent,
child: Stack(
children: [
Positioned(
left: _popupPosition.dx,
top: _popupPosition.dy,
child: popupBuilder(context),
),
],
),
),
),
);

Overlay.of(context).insert(_createdEntry);
_overlayEntry = _createdEntry;

return _createdEntry;
}

/// Function that bounds given offset to screen sizes
Offset _calculatePosition(BuildContext context, Offset position) {
final _screenSize = MediaQuery.of(context).size;

// todo find window size somehow
const width = 250.0;
const height = 250.0;

final _popupRect = Rect.fromCenter(
center: position,
width: width,
height: height,
);

const _screenBorderPadding = 10.0;

double dx = _popupRect.left;
// limiting X offset
dx = max(_screenBorderPadding, dx);
final rightBorderPosition = dx + width;
final rightScreenBorderOverflow = rightBorderPosition - _screenSize.width;
if (rightScreenBorderOverflow >= 0) {
dx -= rightScreenBorderOverflow + _screenBorderPadding;
}

const _verticalMargin = 30.0;
// under the desired position
double dy = position.dy + _verticalMargin;
final bottomBorderPosition = dy + height;
final bottomScreenBorderOverflow =
bottomBorderPosition - _screenSize.height;
// if not enough space underneath, rendering above the desired position
if (bottomScreenBorderOverflow >= 0) {
final newBottomBorderPosition = position.dy - height;
dy = newBottomBorderPosition - _verticalMargin;
}

return Offset(dx, dy);
}

/// Remove popup
void dismiss() {
_overlayEntry?.remove();
}
}

/// Default mistake window that looks similar to LanguageTool popup
class LanguageToolMistakePopup extends StatelessWidget {
/// [LanguageToolMistakePopup] constructor
Expand All @@ -150,53 +62,58 @@ class LanguageToolMistakePopup extends StatelessWidget {
const _borderRadius = 10.0;
const _mistakeNameFontSize = 13.0;
const _mistakeMessageFontSize = 15.0;
const _replacementButtonsSpacing = 10.0;

return Container(
width: popupRenderer.maxWidth,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
boxShadow: const [BoxShadow(color: Colors.grey, blurRadius: 20)],
color: Colors.white,
borderRadius: BorderRadius.circular(_borderRadius),
),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 250),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
mistake.type.name,
style: TextStyle(
color: Colors.grey.shade700,
fontSize: _mistakeNameFontSize,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 10),
Text(
mistake.message,
style: const TextStyle(
// fontStyle: FontStyle.italic,
fontSize: _mistakeMessageFontSize,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// mistake type
Text(
mistake.type.name,
style: TextStyle(
color: Colors.grey.shade700,
fontSize: _mistakeNameFontSize,
fontWeight: FontWeight.w500,
),
const SizedBox(height: 10),
Wrap(
spacing: 10,
direction: Axis.horizontal,
children: mistake.replacements
.map(
(replacement) => ElevatedButton(
onPressed: () {
controller.replaceMistake(mistake, replacement);
popupRenderer.dismiss();
},
child: Text(replacement),
),
)
.toList(growable: false),
),
const SizedBox(height: 10),

// mistake message
Text(
mistake.message,
style: const TextStyle(
// fontStyle: FontStyle.italic,
fontSize: _mistakeMessageFontSize,
),
],
),
),
const SizedBox(height: 10),

// replacements
Wrap(
spacing: _replacementButtonsSpacing,
direction: Axis.horizontal,
children: mistake.replacements
.map(
(replacement) => ElevatedButton(
onPressed: () {
controller.replaceMistake(mistake, replacement);
popupRenderer.dismiss();
},
child: Text(replacement),
),
)
.toList(growable: false),
),
],
),
);
}
Expand Down
110 changes: 110 additions & 0 deletions lib/utils/popup_overlay_renderer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import 'dart:math';

import 'package:flutter/material.dart';

///
const defaultPopupWidth = 250.0;

/// Renderer used to show popup window overlay
class PopupOverlayRenderer {
OverlayEntry? _overlayEntry;

/// Max width of popup window
final double maxWidth;

/// [PopupOverlayRenderer] constructor
PopupOverlayRenderer({this.maxWidth = defaultPopupWidth});

/// Render overlay entry on the screen with dismiss logic
OverlayEntry render(
BuildContext context, {
required Offset position,
required WidgetBuilder popupBuilder,
}) {
// final Offset _popupPosition = _calculatePosition(context, position);

final _createdEntry = OverlayEntry(
builder: (context) => GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: dismiss,
child: Material(
color: Colors.transparent,
child: Stack(
children: [
Positioned(
child: CustomSingleChildLayout(
delegate: PopupOverlayLayoutDelegate(maxWidth, position),
child: popupBuilder(context),
),
)
],
),
),
),
);

Overlay.of(context).insert(_createdEntry);
_overlayEntry = _createdEntry;

return _createdEntry;
}

/// Remove popup
void dismiss() {
_overlayEntry?.remove();
}
}

/// Class that calculates where to place popup window on the screen
class PopupOverlayLayoutDelegate extends SingleChildLayoutDelegate {
/// max width of popup window
final double maxWidth;

/// desired position of popup window
final Offset position;

/// [PopupOverlayLayoutDelegate] constructor
const PopupOverlayLayoutDelegate(this.maxWidth, this.position);

@override
Offset getPositionForChild(Size size, Size childSize) {
return _calculatePosition(size, position, childSize);
}

Offset _calculatePosition(Size size, Offset position, Size childSize) {
final _popupRect = Rect.fromCenter(
center: position,
width: childSize.width,
height: childSize.height,
);
const _screenBorderPadding = 10.0;
print("size: $size");

double dx = _popupRect.left;
// limiting X offset
dx = max(_screenBorderPadding, dx);
final rightBorderPosition = dx + childSize.width;
final rightScreenBorderOverflow = rightBorderPosition - size.width;
if (rightScreenBorderOverflow >= 0) {
dx -= rightScreenBorderOverflow + _screenBorderPadding;
}

const _verticalMargin = 30.0;
// under the desired position
double dy = position.dy + _verticalMargin;
final bottomBorderPosition = dy + childSize.height;
final bottomScreenBorderOverflow = bottomBorderPosition - size.height;
// if not enough space underneath, rendering above the desired position
if (bottomScreenBorderOverflow >= 0) {
final newBottomBorderPosition = position.dy - childSize.height;
dy = newBottomBorderPosition - _verticalMargin;
}

return Offset(dx, dy);
}

@override
bool shouldRelayout(covariant PopupOverlayLayoutDelegate oldDelegate) {
return false;
}
}

0 comments on commit 04ceb89

Please sign in to comment.