Skip to content

popover widget - #3544

Merged
hecrj merged 55 commits into
masterfrom
popover-widget
Nov 12, 2026
Merged

hecrj merged 55 commits into
masterfrom
popover-widget

Conversation

@hecrj

@hecrj hecrj commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

This PR adds a new popover widget, which is analogous to a tooltip but instead of popping up when hovered, its visibility is controlled in view logic.

In practice, this should allows user to build all kinds of dropdown menus.

Additionally, the changes here refactor a bunch of other stuff:

  • opaque was promoted to its own widget and gained a new capture_scroll property for capturing scroll wheel events.
  • overlay::Position abstracted away from tooltip::Position; which gains some more functionality (Auto placement with Side preference).
  • tooltip focuses on pure functionality and its contents are raw now; it no longer supports container styling. Just provide your own container instead.
  • A bunch of redundant Renderer: core::Renderer bounds across the entire codebase have been eliminated.
  • Component overlays now invalidate layout on diffing.

A new `popover` widget, an analogue of `tooltip` that appears only when
its base is clicked and is dismissed when the user clicks outside of the
overlay's bounds.

The base is rendered and behaves like a `button`: it is styled with a
`Status`-reactive `Style` (including an `Opened` variant, set while the
popover is shown), it reports a `Pointer` interaction when hovered, and it
toggles the popover on release over the base (a press alone, or a release
elsewhere, does not open it) — just as a button fires on release.

The popover content is drawn directly, leaving its styling to the caller.

Includes a helper function, an example app, and behavioral tests.
The popover no longer owns its open/closed state. Instead, the application
tracks whether the popover is open and provides that state (`is_open`) on
creation, mirroring the value-based pattern of `checkbox`/`slider` and the
app-provided state of `combo_box`.

The base is now a plain element (any element) that loses all button-like
styling, since the application is the one opening the popover: the `Status`,
`Style`, `Catalog`, and related styling API are removed, and the base is a
straight passthrough (no padding, no press tracking, no pointer interaction).

When the user clicks outside of the popover's bounds, the popover notifies the
application of a close request through a new `on_close` handler. It is up to the
application to actually close the popover by providing `is_open = false` on the
next update. Clicking over the base does not request a close, so the base keeps
its own behavior (e.g. a button that toggles the popover).
Delete the `is_open` state altogether. The popover now always displays its
overlay; it is up to the application to remove the popover from the view when
it is "closed" (e.g. by conditionally including it in the view).

The `on_close` handler is still published when the user clicks outside of the
popover's bounds, which is typically how the application decides to remove it
from the view. The base remains a plain element (any element).
Wrap the example's view in `center`, and build the trigger button once in a
variable so it is reused in both cases: standing alone when the popover is
closed, and anchoring the popover when it is open.
The base is a plain passthrough element, so its mouse interaction (e.g. the
`Pointer` of a `button` base) should be delegated to the base content, just
like the other passthrough methods. Without this, hovering the base reported
no interaction.
The `popover` argument is now an `Option`: `Some` displays the overlay (open)
and `None` hides it (closed). The base is always present, so it keeps a stable
identity/state in the view, and the application simply toggles the `Option`
to open or close the popover.

This replaces the previous "remove the popover from the view" approach: the
`diff` now reconciles two children when open and one when closed, and the
overlay is only produced when the popup is `Some`.
Pass the `Popover` directly to `center` (which takes `impl Into<Element>`)
instead of converting it with a redundant `.into()` and an annotated
`let content: Element<_, _>` binding. The intermediate `Element` broke type
inference for the `trigger` button, so this also removes the annotation.
The `is_press` function was only used once, so inline the `matches!`
expression directly at the call site in the `Overlay`'s `update` method.
Format the popover widget, its tests, and the example with rustfmt.
The `Position` enum gains an `Auto` variant, now the default. When used, the
popover is placed on the side of the base with the most available space
(`Bottom` is preferred on a tie). A `position` builder method is added so the
position can be set explicitly after construction.
The popover's position is set via the `new` argument; only the `Position`
itself defaults to `Position::Auto`.
The `position` is now set exclusively through the `position` method, which
defaults to `Position::Auto`. This removes the argument from both
`Popover::new` and the `popover` helper, and updates the doctests, tests, and
example to use the method.
The `Popover` no longer has a `padding` field, `DEFAULT_PADDING`, or a
`padding` builder method. The popup is laid out directly and positioned
according to the `Position`/`gap`; the application controls the popup's own
padding (e.g. by wrapping it in a `container` with padding). The overlay's
layout now returns the popup's node translated to its computed position
instead of wrapping it in a padded `Node::with_children`.
Since the overlay's `layout` now returns the popup's node directly (rather
than wrapping it in a `Node::with_children`), the `Overlay`'s `update`,
`draw`, `mouse_interaction`, and `operate` must use the `layout` they are
given, not `layout.children().next()`. The latter pointed at the popup's first
inner child instead of the popup itself.
…solve`

Introduce `widget::overlay::Position`, a shared position type for popups
(popovers and tooltips) that combines the popover directions (`Auto`, `Top`,
`Bottom`, `Left`, `Right`) with the tooltip's `FollowCursor`. All the
positioning logic previously inlined in the `popover` and `tooltip` overlays
now lives in `Position::resolve`, which computes the popup's rectangle from the
anchored element, the popup size, the gap/padding, the cursor position, and the
viewport.

`popover::Position` converts into it via `From`, and `tooltip::Position` is now
a re-export of `overlay::Position` (so its default is `Position::Auto` rather
than `Position::Top`).
Analogous to `popover`, `tooltip` now exposes a `position` builder method and
`Tooltip::new` / the `tooltip` helper no longer take a `position` argument —
they default to `Position::Auto` (now the default of the shared
`overlay::Position`). Examples and doctests were updated to use `.position(…)`.
`tooltip` no longer has `padding`, `style`, or `class` (nor the
`container::Catalog` bound they required). Like `popover`, the tooltip's
content is laid out and drawn directly, so the application is responsible for
its padding and styling (e.g. by wrapping it in a `container`). The overlay
layout now returns the tooltip's node translated to its resolved position
instead of wrapping it in a padded, styled `Node::with_children`, and `draw`
delegates to the tooltip's own layout. Examples were updated to wrap their
tooltip content in a styled `container`.
`popover` and `tooltip` no longer apply padding (the application styles the
popup), so `Position::resolve`'s `padding` argument was always `0.0`. Remove
it and simplify the offset/rectangle math accordingly.

Move the positioning test out of `tests/popover.rs` (which drove the full
widget/overlay machinery) into a `#[cfg(test)]` module in `overlay::position`,
where it exercises `Position::resolve` directly. Add coverage for the fixed
directions (`Top`/`Bottom`/`Left`/`Right`) and `FollowCursor` alongside the
existing `Auto` case.
Add a new "Tooltips and popovers" screen to the `tour` example that
showcases both widgets side by side: a `tooltip` shown on hover, and a
`popover` (a floating panel anchored to its base) that opens and closes
on click and dismisses when clicking outside. The *Next* button stays
disabled until the popover has been opened at least once.

With both widgets now demonstrated in the tour, remove the standalone
`popover` and `tooltip` examples.
Wrap the tooltip contents in a `container` with padding and the
`rounded_box` style, matching how the popover content is styled and the
styling shown in the `tooltip` widget documentation.
The `Auto` strategy previously picked the side of the base with the most
raw available space, ignoring how large the popup itself was. When the
popup was bigger than that space it would later be snapped back into the
viewport, landing on top of the base element.

Score each side by the space that remains once the popup (and the gap)
is placed there, so `Auto` prefers a side where the popup actually fits
and avoids overlapping the base. Add a unit test covering the case where
the popup does not fit on its previously preferred side.
- Make `overlay::position` a private module in `widget`; `Position`
  stays public through the existing re-export, and its docs move to
  the type itself.
- Rename the `popover` overlay's `popover_tree` field to `tree` and
  build the overlay children without `push`.
- Rename the popup layout and bounds locals to `layout` and `bounds`
  in `popover` and `tooltip`.
- Use `Point::ORIGIN` for the popover's cursor position.
- `Position::resolve` takes a `snap_within_viewport` argument and
  clamps the resolved rectangle into the viewport when it is `true`.
- `popover` and `tooltip` pass their flag to `resolve` instead of
  clamping the rectangle themselves; it still sizes the popup's
  layout limits.
- Test the clamping and shifting of `Position::resolve` directly.
- Introduce a private `Side` for the four sides a popup can be placed
  on, with `available_space`, `offset`, and `with_most_available_space`.
- `Position::resolve` now matches each `Position` variant directly, and
  the side math is exhaustive over `Side`, so no match arm is
  unreachable.
- `Position::resolve` now takes all its arguments in the same
  coordinate space (the one the popup is laid out in), and
  `FollowCursor` no longer computes the space shift.
- `popover` and `tooltip` apply the overlay translation up front to
  the base bounds and, for `tooltip`, the tracked cursor position.
- The `is_over_base` check of `popover` now compares positions in the
  same coordinate space.
The flag can also restore opacity, since the transparent variant keeps
the base element intact. Also sort the `popover` module with the others
in `lib.rs`.
The feature was only required by the `popover` and `layout`
integration tests, which have moved to the `widget` crate.
Master now collects overlays as `Vec<overlay::Element>` and dropped
`overlay::Group`, so:

- `Popover` returns its overlays as a `Vec` and no longer wraps them
  in a group;
- the `tooltip` overlay's `operate` uses the given layout directly,
  as the tooltip node is no longer wrapped;
- update the `component` and `popover` integration tests accordingly.
# Conflicts:
#	Cargo.toml
#	widget/src/helpers.rs
#	widget/src/tooltip.rs
Bring the popover/tooltip work onto master's rewritten core widget API:
- core::Element now lives in core::widget, Layout has no lifetimes, and
  Widget::overlay receives the window size and no longer requires the
  Overlay trait to expose layout().
- Port Tooltip to the new API (2-argument new(), builder position(),
  shared overlay::Position) and Opaque/Popover to generic W/V types.
- Popover now keeps its base widget as a plain child and computes the
  popup layout lazily inside overlay(); outside clicks still notify the
  application through the on_close handler.
- Fold the tooltip example into the tour app and drop the old tooltip
  example; adapt the editor/tour examples and widget tests to the new
  APIs.
Drop the bounds() helper and pass the translated viewport directly to
the overlay's draw layer and the delegated widget methods, consistent
with the tooltip and sticky overlays.
- Apply rustfmt to the popover/tooltip/opaque widget changes
- Link the iced logo helper to crate::Element explicitly, since the
  Element import no longer shadows the name in helpers
- Disambiguate the opaque helper links in the popover docs (the name
  refers to both a function and a module at the crate root)
@hecrj hecrj added this to the 0.15 milestone Nov 12, 2026
@hecrj hecrj added improvement An internal improvement feature New feature or request widget addition labels Nov 12, 2026
@hecrj
hecrj enabled auto-merge November 12, 2026 12:14
@hecrj
hecrj merged commit 23b303c into master Nov 12, 2026
30 checks passed
@hecrj
hecrj deleted the popover-widget branch November 12, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

addition feature New feature or request improvement An internal improvement widget

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant