Skip to content

fix: Popover positioning#111

Open
userFortyTwo wants to merge 4 commits into
minikin:mainfrom
userFortyTwo:main
Open

fix: Popover positioning#111
userFortyTwo wants to merge 4 commits into
minikin:mainfrom
userFortyTwo:main

Conversation

@userFortyTwo

@userFortyTwo userFortyTwo commented Feb 24, 2026

Copy link
Copy Markdown

This pull request fixes issue #103 ("[Bug]: Popover is not centered above widget", https://github.com/minikin/popover/issues/103), which, like all other issues, seems to have been deleted for some reason.

Example

The following example can be used to reproduce the issue:

import 'package:flutter/material.dart';
import 'package:popover/popover.dart';

void main() {
  runApp(const MyApp());
}

final class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: LayoutBuilder(
            builder: (context, constraints) {
              return GestureDetector(
                onTap: () => _showPopover(context),
                child: Container(
                  height: constraints.maxHeight / 2.0,
                  color: Colors.red,
                ),
              );
            },
          ),
        ),
      ),
    );
  }

  void _showPopover(BuildContext context) {
    showPopover(
      context: context,
      bodyBuilder: (_) => const Center(
        child: Text('Popover!', style: TextStyle(fontSize: 18)),
      ),
      direction: PopoverDirection.top,
      width: 150,
      height: 60,
    );
  }
}

Without this pull request, the result will look like so:

bug

With the pull request in place, this will be the result:

fixed

Summary by CodeRabbit

  • Refactor
    • Popover positioning now prefers centered placement near its attach area and enforces 8px horizontal and vertical margins. Popovers are clamped within screen bounds, reducing overflow and improving consistency across screen sizes and orientations.

@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 17463000-a09c-40ba-9dce-8642de0c1871

📥 Commits

Reviewing files that changed from the base of the PR and between 851f4d6 and 0fe4e46.

📒 Files selected for processing (1)
  • lib/src/popover_position_render_object.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/src/popover_position_render_object.dart

📝 Walkthrough

Walkthrough

The popover positioning now centers the popover on the attach rect by default and clamps horizontal/vertical offsets to fixed 8.0-pixel margins when centering would overflow the physical screen; branching and fallback logic tied to arrowHeight was removed. Public signatures unchanged.

Changes

Cohort / File(s) Summary
Popover Position Layout
lib/src/popover_position_render_object.dart
Rewrote _horizontalOffset() and _verticalOffset() to compute center-based offsets (attach rect center minus half popover size) and clamp results to horizontalMargin/verticalMargin (8.0px); removed previous arrow-height-based fallback positioning and simplified branching/formatting in offset calculation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • minikin

Poem

🐰 I hopped along the layout trail,
Centered first, I trimmed the tail,
Margins kept my bounds in sight,
No more drifting left or right,
Popovers snug—goodnight, goodnight. 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: Popover positioning' directly describes the primary change—addressing the popover positioning bug (issue #103)—and aligns with the changeset updates to horizontal/vertical offset calculations and margin-based clamping logic.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
lib/src/popover_position_render_object.dart (1)

118-139: Same indentation and shared-constant concerns as _horizontalOffset above.

The centering logic itself is correct and mirrors the horizontal approach appropriately.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/popover_position_render_object.dart` around lines 118 - 139, The
_verticalOffset method uses a local const verticalMargin and inconsistent
indentation; mirror the fix from _horizontalOffset by replacing the local
verticalMargin with the shared margin constant used elsewhere (e.g. the same
top-level/shared const name used by _horizontalOffset) and adjust the
indentation to match surrounding code style; ensure you reference attachRect,
PopoverUtils.physicalSize and size.height in the same ordering/format as
_horizontalOffset so both functions share the same margin constant and
consistent formatting.
🧹 Nitpick comments (1)
lib/src/popover_position_render_object.dart (1)

95-116: Misleading indentation: if/else bodies are at the same level as the control-flow keywords.

Lines 105–106, 108–109, and 111–112 are indented at the same depth as the if/else if/else they belong to, making the control flow hard to follow at a glance. The same issue is present in _verticalOffset below.

Also, the 8.0 margin is duplicated as a local const in both _horizontalOffset and _verticalOffset. Consider extracting it to a single class-level or file-level constant (or making it a constructor parameter for configurability).

♻️ Suggested fix (indentation + shared constant)
+  static const _defaultMargin = 8.0;
+
   double _horizontalOffset(Size size) {
     var offset = 0.0;
-    const horizontalMargin = 8.0; // Proper horizontal margin instead of arrowHeight
 
     // Always try to center the popover first
     final centeredOffset = attachRect.left + attachRect.width / 2 - size.width / 2;
 
     // Check if centered popover would fit on screen with margins
-    if (centeredOffset >= horizontalMargin &&
-      centeredOffset + size.width <= PopoverUtils.physicalSize.width - horizontalMargin) {
-    // Perfect centering case
-    offset = centeredOffset;
-    } else if (centeredOffset < horizontalMargin) {
-    // Popover would go off left edge, align to left margin
-    offset = horizontalMargin;
+    if (centeredOffset >= _defaultMargin &&
+        centeredOffset + size.width <= PopoverUtils.physicalSize.width - _defaultMargin) {
+      // Perfect centering case
+      offset = centeredOffset;
+    } else if (centeredOffset < _defaultMargin) {
+      // Popover would go off left edge, align to left margin
+      offset = _defaultMargin;
     } else {
-    // Popover would go off right edge, align to right margin
-    offset = PopoverUtils.physicalSize.width - horizontalMargin - size.width;
+      // Popover would go off right edge, align to right margin
+      offset = PopoverUtils.physicalSize.width - _defaultMargin - size.width;
     }
 
     return offset;
   }

Apply the same indentation fix and constant usage to _verticalOffset.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/popover_position_render_object.dart` around lines 95 - 116, The
if/else branches in _horizontalOffset (and likewise in _verticalOffset) are
misindented and hard to read; re-indent the bodies so they sit inside their
respective if/else blocks and mirror that fix in _verticalOffset, and remove the
duplicated local const horizontalMargin = 8.0 by extracting a shared constant
(e.g., const double _popoverMargin = 8.0 at file- or class-level or accept it as
a constructor parameter) and use that single symbol in both _horizontalOffset
and _verticalOffset.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@lib/src/popover_position_render_object.dart`:
- Around line 118-139: The _verticalOffset method uses a local const
verticalMargin and inconsistent indentation; mirror the fix from
_horizontalOffset by replacing the local verticalMargin with the shared margin
constant used elsewhere (e.g. the same top-level/shared const name used by
_horizontalOffset) and adjust the indentation to match surrounding code style;
ensure you reference attachRect, PopoverUtils.physicalSize and size.height in
the same ordering/format as _horizontalOffset so both functions share the same
margin constant and consistent formatting.

---

Nitpick comments:
In `@lib/src/popover_position_render_object.dart`:
- Around line 95-116: The if/else branches in _horizontalOffset (and likewise in
_verticalOffset) are misindented and hard to read; re-indent the bodies so they
sit inside their respective if/else blocks and mirror that fix in
_verticalOffset, and remove the duplicated local const horizontalMargin = 8.0 by
extracting a shared constant (e.g., const double _popoverMargin = 8.0 at file-
or class-level or accept it as a constructor parameter) and use that single
symbol in both _horizontalOffset and _verticalOffset.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b65e783 and 28ff8fe.

📒 Files selected for processing (1)
  • lib/src/popover_position_render_object.dart

@minikin

minikin commented Mar 4, 2026

Copy link
Copy Markdown
Owner

@userFortyTwo thank you for your contribution!
Can you please remove unnecessary comments and record a demo? We also need to fix CI.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/src/popover_position_render_object.dart`:
- Around line 95-129: The clamping logic in _horizontalOffset(Size size) and
_verticalOffset(Size size) currently uses PopoverUtils.physicalSize (device
screen) but should use the render object's actual bounds set in performLayout;
replace references to PopoverUtils.physicalSize.width/height with
this.size.width/height (the render object's size) when checking and computing
the max offset and fallback positions so the popover is clamped to the render
object's bounds rather than the full screen.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: afd88cd9-b1f5-4e5e-807d-f17c4a94e880

📥 Commits

Reviewing files that changed from the base of the PR and between 28ff8fe and 851f4d6.

📒 Files selected for processing (1)
  • lib/src/popover_position_render_object.dart

Comment on lines 95 to +129
double _horizontalOffset(Size size) {
var offset = 0.0;
const horizontalMargin = 8.0;

if (attachRect.left > size.width / 2 &&
PopoverUtils.physicalSize.width - attachRect.right > size.width / 2) {
offset = attachRect.left + attachRect.width / 2 - size.width / 2;
} else if (attachRect.left < size.width / 2) {
offset = arrowHeight;
final centeredOffset =
attachRect.left + attachRect.width / 2 - size.width / 2;

if (centeredOffset >= horizontalMargin &&
centeredOffset + size.width <=
PopoverUtils.physicalSize.width - horizontalMargin) {
offset = centeredOffset;
} else if (centeredOffset < horizontalMargin) {
offset = horizontalMargin;
} else {
offset = PopoverUtils.physicalSize.width - arrowHeight - size.width;
offset = PopoverUtils.physicalSize.width - horizontalMargin - size.width;
}

return offset;
}

double _verticalOffset(Size size) {
var offset = 0.0;
const verticalMargin = 8.0;

if (attachRect.top > size.height / 2 &&
PopoverUtils.physicalSize.height - attachRect.bottom >
size.height / 2) {
offset = attachRect.top + attachRect.height / 2 - size.height / 2;
} else if (attachRect.top < size.height / 2) {
offset = arrowHeight;
final centeredOffset =
attachRect.top + attachRect.height / 2 - size.height / 2;

if (centeredOffset >= verticalMargin &&
centeredOffset + size.height <=
PopoverUtils.physicalSize.height - verticalMargin) {
offset = centeredOffset;
} else if (centeredOffset < verticalMargin) {
offset = verticalMargin;
} else {
offset = PopoverUtils.physicalSize.height - arrowHeight - size.height;
offset = PopoverUtils.physicalSize.height - verticalMargin - size.height;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file under review
head -150 lib/src/popover_position_render_object.dart | tail -60

Repository: minikin/popover

Length of output: 1712


🏁 Script executed:

# Check the PopoverUtils definition
find . -name "popover_utils.dart" -type f

Repository: minikin/popover

Length of output: 93


🏁 Script executed:

# Look at the performLayout method to understand the context
sed -n '40,80p' lib/src/popover_position_render_object.dart

Repository: minikin/popover

Length of output: 1185


🏁 Script executed:

# Check PopoverUtils.physicalSize definition
cat lib/src/utils/popover_utils.dart

Repository: minikin/popover

Length of output: 1436


🏁 Script executed:

# Find which widgets use PopoverPositionRenderObject
rg "PopoverPositionRenderObject" --type dart

Repository: minikin/popover

Length of output: 405


🏁 Script executed:

# Look for the parent widget that creates this render object
rg "createRenderObject" lib/src/ --type dart -A 5

Repository: minikin/popover

Length of output: 889


🏁 Script executed:

# Check for any CompositedTransformFollower or similar positioning
rg "CompositedTransformFollower\|Positioned\|Overlay" lib/src/ --type dart

Repository: minikin/popover

Length of output: 41


🏁 Script executed:

# Look at the full popover_position_render_object.dart to understand the class
wc -l lib/src/popover_position_render_object.dart

Repository: minikin/popover

Length of output: 106


🏁 Script executed:

# Read the full file to understand context
cat lib/src/popover_position_render_object.dart

Repository: minikin/popover

Length of output: 3736


🏁 Script executed:

# Look at the widget that uses this render object
cat lib/src/popover_position_widget.dart

Repository: minikin/popover

Length of output: 1055


🏁 Script executed:

# Check how PopoverPositionWidget is used
rg "PopoverPositionWidget" --type dart -B 2 -A 2

Repository: minikin/popover

Length of output: 1008


🏁 Script executed:

# Look at the popover context to understand the full layout structure
head -100 lib/src/popover_context.dart

Repository: minikin/popover

Length of output: 1612


🏁 Script executed:

# Look at PopoverItem to see how PopoverPositionWidget is used
cat lib/src/popover_item.dart

Repository: minikin/popover

Length of output: 4384


🏁 Script executed:

# Check the Stack and how constraints flow through
rg "Stack\|Dialog\|ConstrainedBox" lib/src/ --type dart -B 2 -A 2

Repository: minikin/popover

Length of output: 41


🏁 Script executed:

# Let's understand the coordinate system - check if there are any tests
find . -name "*test*" -type f | grep -i popover

Repository: minikin/popover

Length of output: 83


🏁 Script executed:

# Look at how Popover is shown - the entry point
rg "showPopover" lib/src/ --type dart -A 10

Repository: minikin/popover

Length of output: 797


🏁 Script executed:

# Search for any documentation or examples
find . -name "*.md" -o -name "example*" | head -10

Repository: minikin/popover

Length of output: 328


🏁 Script executed:

# Let's look at the test file to understand typical usage
cat test/popover_test.dart

Repository: minikin/popover

Length of output: 14106


🏁 Script executed:

# Check the example to see how it's typically used
ls -la example/lib/

Repository: minikin/popover

Length of output: 231


🏁 Script executed:

# Look for any issues or discussions about positioning
find . -name "*.md" -exec grep -l "position\|overlay\|constrain" {} \;

Repository: minikin/popover

Length of output: 73


🏁 Script executed:

# Let's check PopoverRenderShiftedBox to see if it has similar logic
cat lib/src/popover_render_shifted_box.dart | head -100

Repository: minikin/popover

Length of output: 2499


Use the render box bounds for clamping instead of the global screen size.

The new center-first positioning logic is better, but _horizontalOffset() and _verticalOffset() still clamp against PopoverUtils.physicalSize, which is the raw device screen size. When PopoverPositionRenderObject is constrained to bounds smaller than the full screen (e.g., within dialogs or other constrained layouts), this can position the popover outside its actual render bounds. Since performLayout() sets this.size before calculateOffset() runs, the clamping should use the render object's actual bounds instead of a global screen size.

🛠️ Proposed fix
-  double _horizontalOffset(Size size) {
+  double _horizontalOffset(Size childSize) {
     var offset = 0.0;
     const horizontalMargin = 8.0;
+    final availableWidth = this.size.width;

     final centeredOffset =
-        attachRect.left + attachRect.width / 2 - size.width / 2;
+        attachRect.left + attachRect.width / 2 - childSize.width / 2;

     if (centeredOffset >= horizontalMargin &&
-        centeredOffset + size.width <=
-            PopoverUtils.physicalSize.width - horizontalMargin) {
+        centeredOffset + childSize.width <= availableWidth - horizontalMargin) {
       offset = centeredOffset;
     } else if (centeredOffset < horizontalMargin) {
       offset = horizontalMargin;
     } else {
-      offset = PopoverUtils.physicalSize.width - horizontalMargin - size.width;
+      offset = availableWidth - horizontalMargin - childSize.width;
     }

     return offset;
   }

-  double _verticalOffset(Size size) {
+  double _verticalOffset(Size childSize) {
     var offset = 0.0;
     const verticalMargin = 8.0;
+    final availableHeight = this.size.height;

     final centeredOffset =
-        attachRect.top + attachRect.height / 2 - size.height / 2;
+        attachRect.top + attachRect.height / 2 - childSize.height / 2;

     if (centeredOffset >= verticalMargin &&
-        centeredOffset + size.height <=
-            PopoverUtils.physicalSize.height - verticalMargin) {
+        centeredOffset + childSize.height <=
+            availableHeight - verticalMargin) {
       offset = centeredOffset;
     } else if (centeredOffset < verticalMargin) {
       offset = verticalMargin;
     } else {
-      offset = PopoverUtils.physicalSize.height - verticalMargin - size.height;
+      offset = availableHeight - verticalMargin - childSize.height;
     }

     return offset;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/popover_position_render_object.dart` around lines 95 - 129, The
clamping logic in _horizontalOffset(Size size) and _verticalOffset(Size size)
currently uses PopoverUtils.physicalSize (device screen) but should use the
render object's actual bounds set in performLayout; replace references to
PopoverUtils.physicalSize.width/height with this.size.width/height (the render
object's size) when checking and computing the max offset and fallback positions
so the popover is clamped to the render object's bounds rather than the full
screen.

@userFortyTwo

Copy link
Copy Markdown
Author

@minikin May I ask where all your issues went? Issue #103 did in fact already contain a minimum working example.

Anyway, I have created a new example demonstrating the issue and have updated the description of this pull request.

I have also removed all comments generated by the LLM and fixed the indentation issues. And I have now given maintainers of this repo write access to the pull request.

coderabbitai seems to be reporting issues with the commits. So you may want to review that the changes do not introduce new problems.

Let me know if there's anything else I can do.

@userFortyTwo userFortyTwo removed their assignment Mar 29, 2026
@minikin

minikin commented Mar 30, 2026

Copy link
Copy Markdown
Owner

@userFortyTwo

Thanks for the update and for taking the time to address the feedback. I appreciate it.

Regarding the issues, I intentionally chose to disable them. After many years as an open source maintainer and contributor, I’ve noticed that almost all issues, about 99%, are requests for me to fix things, not actual contributions. That approach just isn’t sustainable for a project I work on in my free time.

I prefer to keep things simple and focus on pull requests.
If someone is willing to submit a working change, I’m happy to have that conversation.

PS: I've updated README, and CONTRIBUTING

@minikin minikin changed the title Fix popover positioning (#103, deleted) fix: Popover positioning Mar 30, 2026
@minikin

minikin commented Mar 30, 2026

Copy link
Copy Markdown
Owner

@userFortyTwo, please, fix CI.

@userFortyTwo

Copy link
Copy Markdown
Author

@minikin Might be fixed now (pipeline awaiting approval)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants