Watch
1
0
Fork
You've already forked souveraine
0

Selection: fix untappable chip and the chip that never cleared

Two bugs from the first on-device run.

The card sized itself from a layout it also centered, which resolved to zero, so
mask claimed a zero-size input region: the chip painted but no tap landed. Card
now sizes from the layout's implicit size and the background is a plain Item
child, not layout-managed.

wl-paste --watch replays the existing selection on start, so every watcher
restart (every unlock) resurrected a stale selection that could never be cleared
because the primary buffer never changed again. The first emission after a start
now only sets the baseline, and a dismissed selection is remembered so a re-read
cannot revive it.
This commit is contained in:
Fimeg 2026-07-29 18:05:49 -04:00
commit 5a643d7059
2 changed files with 64 additions and 23 deletions

View file

@ -92,12 +92,22 @@ Scope {
return Math.min(win.hintY + 16, win.height - card.height - win.pad);
}
// `card` takes its size from the layout's implicit size, and the
// layout takes its size from its own no path where a child's size
// depends on the parent's. The earlier version centered the layout
// in the card while the card sized from the layout, which resolved
// to zero and silently made `mask` a zero-size input region: the
// chip painted but nothing could be tapped. The background stays a
// plain Item child, because anchors on a layout-managed item are
// undefined behavior.
Item {
id: card
x: win.targetX
y: win.targetY
width: content.implicitWidth
height: content.implicitHeight
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight
width: implicitWidth
height: implicitHeight
// Position eases so a re-selection slides rather than teleports,
// but only once mapped animating from 0,0 on first show reads
@ -113,6 +123,7 @@ Scope {
Rectangle {
anchors.fill: parent
z: -1
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
border.width: 1
@ -121,33 +132,36 @@ Scope {
ColumnLayout {
id: content
anchors.centerIn: parent
anchors.left: parent.left
anchors.top: parent.top
width: implicitWidth
height: implicitHeight
spacing: 0
// collapsed: the chip
SelectionChip {
visible: !scope.expanded
charCount: Selection.text.length
onTapped: {
DeviceEvidence.touched("selection-menu");
Haptics.trigger("button-pressed");
scope.expanded = true;
}
onDismissed: {
DeviceEvidence.touched("selection-menu");
Selection.dismiss();
}
// collapsed: the chip
SelectionChip {
visible: !scope.expanded
charCount: Selection.text.length
onTapped: {
DeviceEvidence.touched("selection-menu");
Haptics.trigger("button-pressed");
scope.expanded = true;
}
onDismissed: {
DeviceEvidence.touched("selection-menu");
Selection.dismiss();
}
}
// expanded: the actions
SelectionActions {
visible: scope.expanded
onActed: {
DeviceEvidence.touched("selection-menu");
Selection.dismiss();
}
// expanded: the actions
SelectionActions {
visible: scope.expanded
onActed: {
DeviceEvidence.touched("selection-menu");
Selection.dismiss();
}
}
}
}
}
}

View file

@ -88,6 +88,7 @@ Singleton {
function _reconcile() {
if (root._permitted) {
if (!watcher.running) {
root._primed = false;
watcher.running = true;
console.log("[Selection] watching primary selection");
}
@ -111,12 +112,26 @@ Singleton {
// Called by the menu when the user dismisses it or an action consumes the
// selection. Does not touch the compositor's selection only our view of it.
//
// The dismissed text is remembered: the compositor's selection is unchanged
// by dismissing, so any later re-read of it (a watcher restart) would
// otherwise resurrect the chip the user just closed.
function dismiss() {
settleTimer.running = false;
root._dismissedText = root.text;
root._clear();
}
property string _pending: ""
property string _dismissedText: ""
// wl-paste --watch fires ONCE IMMEDIATELY with whatever the selection
// already holds, before any new user action. That replay is not a fresh
// selection: it resurrected an hour-old selection every time the watcher
// restarted (i.e. on every unlock), leaving a chip that could never be
// cleared because the primary buffer never changed again. The first
// emission after a start only establishes the baseline.
property bool _primed: false
Component.onCompleted: root._reconcile()
@ -147,6 +162,16 @@ Singleton {
function _onSelection(raw) {
if (!root._permitted) return;
const t = String(raw ?? "");
// The startup replay: record it as already-seen and show nothing.
if (!root._primed) {
root._primed = true;
root._dismissedText = t;
console.log(`[Selection] baseline ${t.trim().length} chars (not shown)`);
return;
}
// A re-read of something the user already dismissed is not a new
// selection. Only an actual change re-opens the chip.
if (t.length > 0 && t === root._dismissedText) return;
if (t.trim().length < root._minChars) {
// A cleared or trivial selection retires the menu rather than
// leaving a stale one anchored over nothing.
@ -154,6 +179,8 @@ Singleton {
root._clear();
return;
}
// A genuinely new selection clears the dismissal memory.
root._dismissedText = "";
root._pending = t;
// Restart the settle window on every growth tick.
settleTimer.restart();