Skip to content

FocusScope consumes the pointer press that gives it focus - first click into an unfocused scope never reaches other handlers #12044

@Eyalm321

Description

@Eyalm321

Feature Description

FocusScope::input_event consumes the pointer press that gives the scope focus: when the scope is enabled, focus-on-click is true (the default), and it does not already have focus, a Pressed event grabs focus and returns EventAccepted (internal/core/items/input_items.rs, FocusScope::input_event).

The press therefore never reaches other pointer handlers under the cursor, producing the classic "click once to focus, click again to act" behavior: the first click into an unfocused widget does nothing visible. Note the asymmetry — once the scope has focus, the same press returns EventIgnored and propagates normally, so only the focus-delivering press is special.

I'd like an opt-in way for a FocusScope to take focus on press and let the press propagate.

Minimal repro

export component Demo inherits Window {
    width: 200px;
    height: 200px;

    ta := TouchArea {
        pointer-event(ev) => {
            if ev.kind == PointerEventKind.down {
                debug("press"); // does NOT fire on the first click
            }
        }
        clicked => { debug("clicked"); } // does NOT fire on the first click either
    }

    // Later sibling, drawn above `ta`, hit-tested first — the common
    // "widget with both pointer handling and keyboard handling" shape.
    fs := FocusScope { }
}

First click: fs grabs focus and swallows the press, so ta never sees the button-down (clicked also stays silent because the press never armed the TouchArea). Every subsequent click works. The same applies when the FocusScope wraps the TouchArea and the inner TouchArea doesn't cover the press position, or generally whenever an unfocused FocusScope wins the hit-test.

Real-world impact

In a terminal-emulator widget (FocusScope for keyboard input + TouchArea for the pointer), this single root cause produced two user-visible bugs:

  • right-click-paste into an unfocused pane silently no-opped (paste was triggered on button-down, which vanished);
  • a drag-selection's anchor press was lost when the pane wasn't focused, so the drag selected nothing — and with copy-on-select enabled, the release re-copied a stale selection, clobbering the clipboard.

Workaround today

focus-on-click: false; on the scope plus calling fs.focus() from the TouchArea's pointer-event on every button-down. Works, but it's boilerplate every "focusable pointer widget" must rediscover, and the failure mode (silently lost first press) is hard to diagnose.

Prior art

Proposed API

A third boolean alongside focus-on-click / focus-on-tab-navigation (per the API-review preference for bools in #9263), e.g.:

/// When false, the pointer press that gives this FocusScope the focus via
/// `focus-on-click` is not consumed: after focus is transferred, the press
/// continues to be processed by elements behind the FocusScope.
/// Note: with `false`, an enclosing FocusScope with default settings may then
/// take the focus instead (see #2530); opt in on outer scopes accordingly.
in property <bool> consume-focus-click: true;

(An enum on focus-on-click, e.g. pass-through, would express the same thing but would be a breaking type change to an existing bool property.)

Implementation is a small change in FocusScope::input_event: return EventIgnored instead of EventAccepted after set_focus_item(...) when the property is false. I'm happy to submit a PR with the property, tests, docs, and CHANGELOG entry.

Product Impact

Any widget that combines a FocusScope with sibling/overlapping pointer handlers (terminal emulators, canvases, custom list items) currently loses the first pointer press into an unfocused instance, or must carry the manual-focus workaround.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions