GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix: a popover drawn away from its laid-out rect is occluded where it is drawn
Application::popover_offset says where a widget's open popover is drawn
relative to the rect it reports. An app that lays its page out unscrolled
and shifts what it emits (cce-system-interface) drew a dropdown's menu
scroll_y above the rect the engine clamped text against, so on a scrolled
page the page's text was drawn over the menu, the menu's own labels were
cut by their own rect, and a menu-shaped hole was cut in the text one
scroll offset below. The text clamp, the overflow input region, the
popover region sent to the compositor and the outside-press close all
read the rect through the offset now. The default is zero, so no
existing app changes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
src/backend/window_runner.rs | 39 ++++++++++++++++++++++++++++++++++-----
src/context.rs | 24 ++++++++++++++++++++----
2 files changed, 54 insertions(+), 9 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 9f97d5e..43fc11b 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -3624,6 +3624,20 @@ pub trait Application: Sized + 'static {
None
}
+ /// Where a widget's open popover is DRAWN, as an offset from the rect it
+ /// reports (`popover_rect`). A widget reports in the coordinates it was
+ /// laid out in; an app that lays its page out unscrolled and shifts what
+ /// it emits draws the popover `scroll` px away from there, and returns
+ /// `(0.0, -scroll_y)` here for the page's widgets. Everything the engine
+ /// derives from a popover rect reads it through this: the text-occlusion
+ /// clamp, the overflow input region, and the region sent to the
+ /// compositor. Without it a menu opened on a scrolled page had the page's
+ /// text drawn over it, and cut a menu-shaped hole in the text one scroll
+ /// offset away.
+ fn popover_offset(&self, _id: crate::widget::WidgetId) -> (f32, f32) {
+ (0.0, 0.0)
+ }
+
/// Whether a left-press at (px, py) should start a compositor window drag. Every root
/// root plate container is dissolved (Phase 6), so the default is "no" — apps that want
/// drag-anywhere override this with `ctx.drag_allowed_at(px, py)`.
@@ -4129,13 +4143,15 @@ impl<A: Application> EngineState<A> {
wl_region.add(0, 0, gw, gh);
// Open popovers, clamped to the surface.
if let Some(ctx) = self.inner.as_ref().unwrap().ui_context() {
- for (_id, ptr) in ctx.tree.iter_registered() {
+ for (id, ptr) in ctx.tree.iter_registered() {
unsafe {
let Some(w) = ptr.as_ref() else { continue };
if !w.visible() {
continue;
}
let Some((px, py, pw, ph)) = w.popover_rect() else { continue };
+ let (dx, dy) = self.inner.as_ref().unwrap().popover_offset(id);
+ let (px, py) = (px + dx, py + dy);
let x0 = px.max(0.0) as i32;
let y0 = py.max(0.0) as i32;
let x1 = ((px + pw).min(self.logical_width)) as i32;
@@ -4169,13 +4185,15 @@ impl<A: Application> EngineState<A> {
}
let mut union: Option<(f32, f32, f32, f32)> = None;
if let Some(ctx) = self.inner.as_ref().unwrap().ui_context() {
- for (_id, ptr) in ctx.tree.iter_registered() {
+ for (id, ptr) in ctx.tree.iter_registered() {
unsafe {
let Some(w) = ptr.as_ref() else { continue };
if !w.visible() {
continue;
}
let Some((px, py, pw, ph)) = w.popover_rect() else { continue };
+ let (dx, dy) = self.inner.as_ref().unwrap().popover_offset(id);
+ let (px, py) = (px + dx, py + dy);
let (x0, y0) = (px.max(0.0), py.max(0.0));
let x1 = (px + pw).min(self.logical_width);
let y1 = (py + ph).min(self.logical_height);
@@ -4399,7 +4417,8 @@ impl<A: Application> EngineState<A> {
if let Some(ptr) = ctx.tree.get_ptr(pop_id) {
unsafe {
if let Some((x, y, w, h)) = (*ptr).popover_rect() {
- dl_overlay_rects.push((x, y, w, h));
+ let (dx, dy) = self.inner.as_ref().unwrap().popover_offset(pop_id);
+ dl_overlay_rects.push((x + dx, y + dy, w, h));
}
}
}
@@ -5034,8 +5053,18 @@ impl<A: Application> PointerHandler for EngineState<A> {
// dispatch: apps commonly region-gate their routing, so an
// open menu's owner may never hear about a press elsewhere.
if btn == MouseButton::Left {
- if let Some(ctx) = self.inner.as_mut().unwrap().ui_context_mut() {
- ctx.close_popovers_missed_by_press(lx, ly);
+ let app = self.inner.as_mut().unwrap();
+ let offsets: Vec<_> = app
+ .ui_context()
+ .map(|ctx| ctx.popover_owners())
+ .unwrap_or_default()
+ .into_iter()
+ .map(|id| (id, app.popover_offset(id)))
+ .collect();
+ if let Some(ctx) = app.ui_context_mut() {
+ ctx.close_popovers_missed_by_press_with(lx, ly, |id| {
+ offsets.iter().find(|(o, _)| *o == id).map_or((0.0, 0.0), |&(_, d)| d)
+ });
}
}
diff --git a/src/context.rs b/src/context.rs
index 6da6713..3cdd45c 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -825,17 +825,33 @@ impl UiContext {
/// second delivery through the app's own dispatch is idempotent (a closing
/// dropdown ignores further presses).
pub fn close_popovers_missed_by_press(&mut self, x: f32, y: f32) {
- let owners: Vec<WidgetId> = self
- .tree
+ self.close_popovers_missed_by_press_with(x, y, |_| (0.0, 0.0));
+ }
+
+ /// The widgets with an open popover, in registry order.
+ pub fn popover_owners(&self) -> Vec<WidgetId> {
+ self.tree
.iter_registered()
.filter_map(|(id, ptr)| unsafe {
ptr.as_ref().and_then(|w| {
(w.visible() && w.popover_rect().is_some()).then_some(id)
})
})
- .collect();
- for id in owners {
+ .collect()
+ }
+
+ /// [`close_popovers_missed_by_press`](Self::close_popovers_missed_by_press)
+ /// for a press in SURFACE coordinates: `offset` is where each owner is
+ /// drawn relative to where it was laid out
+ /// (`Application::popover_offset`), and the press is carried back into
+ /// the owner's own coordinates before it is tested and delivered. On a
+ /// scrolled page the unshifted test called a press on a menu row a miss,
+ /// and closed the menu under the click.
+ pub fn close_popovers_missed_by_press_with(&mut self, x: f32, y: f32, offset: impl Fn(WidgetId) -> (f32, f32)) {
+ for id in self.popover_owners() {
let Some(ptr) = self.tree.get_ptr(id) else { continue };
+ let (dx, dy) = offset(id);
+ let (x, y) = (x - dx, y - dy);
unsafe {
if !(*ptr).hit_test(x, y, self) {
let ev = Event::MouseButton {