fix(qt): move the cortinilla debounce from QML into Rust
The 220ms debounce wrapped the WHOLE searchLive invokable in QML, so live()
- which opens the dropdown and raises the skeleton - only ran 220ms after
the LAST keystroke. Continuous typing therefore showed nothing at all. The
reference opens the panel and raises the skeleton on the FIRST keystroke of
2+ characters and debounces only the LOAD.
live() is now two halves. The synchronous half runs on the first poll of the
spawned task, i.e. at the keystroke: gates, selection reset, open, query,
version bump, loading flag. Then it sleeps 220ms and re-checks the version,
so a superseded task exits without touching the network. This is the shape
the immersive arm already uses (imm_live), which is the precedent in both
respects: raise synchronously, debounce the load.
cortinillaLoading gets exactly one owner in the synchronous half and is
written once more at the publish - never in both halves for the same
keystroke.
Two consequences ride along:
- dismiss() becomes a real cancellation point. It only flipped the open
flag, so an in-flight load survived Esc, published into a closed
dropdown and rewrote LAST_CORT. It now bumps the version too. This
matters more now that the version bumps per keystroke.
- the idle-close timer restarts on the QUERY rather than the payload. A
burst of typing publishes one payload but many queries, and the
reference restarts per keystroke.
HeaderBar.qml's header claimed the cortinilla and live-search were "out of
scope" in the file that wires the whole input path. Corrected here, beside
the code that falsifies it, rather than in a later docs sweep.
Contract: qbz-nix-docs/qt-frontend/2026-08-03-cortinilla-parity, commit C3
(H1/H2/H3/H8, D-BUG-8, R7-4).
Gate: build clean, 256/256 tests, both QML audits OK, offscreen smoke
0 complaints + home published.
cc
This commit is contained in:
3 files changed
+52
-18
No files matched your search
@@ -61,8 +61,11 @@ Item {
|
|||||||
}
|
}
|
||||||
Connections {
|
Connections {
|
||||||
target: QbzSearch
|
target: QbzSearch
|
||||||
// Activity restarts the countdown (keystroke or arrow move).
|
// Activity restarts the countdown (keystroke or arrow move). The
|
||||||
function onCortinillaJsonChanged() { if (root.visible && !root.panelHovered) idleClose.restart() }
|
// keystroke probe is the QUERY, not the payload: the reference
|
||||||
|
// restarts per keystroke, and now that the debounce lives in Rust a
|
||||||
|
// burst of typing publishes one payload but many queries.
|
||||||
|
function onCortinillaQueryChanged() { if (root.visible && !root.panelHovered) idleClose.restart() }
|
||||||
function onCortinillaSelectedIndexChanged() { if (root.visible && !root.panelHovered) idleClose.restart() }
|
function onCortinillaSelectedIndexChanged() { if (root.visible && !root.panelHovered) idleClose.restart() }
|
||||||
// Keyboard scroll-into-view: the controller publishes the selected
|
// Keyboard scroll-into-view: the controller publishes the selected
|
||||||
// row's content-space top-y; nudge the Flickable so it is visible.
|
// row's content-space top-y; nudge the Flickable so it is visible.
|
||||||
|
|||||||
@@ -15,8 +15,12 @@
|
|||||||
//
|
//
|
||||||
// The search field gives up 60px whenever the nav is in the header, and
|
// The search field gives up 60px whenever the nav is in the header, and
|
||||||
// re-centers with a 220ms animation (HeaderBar.slint:569).
|
// re-centers with a 220ms animation (HeaderBar.slint:569).
|
||||||
// Center: the search field, absolutely centered on the window (VISUAL
|
// Center: the search field, absolutely centered on the window. It owns the
|
||||||
// replica — the cortinilla/live-search is out of scope; POC-NOTE).
|
// live-search input path: the >= 2-char gate and the dismiss below it, the
|
||||||
|
// arrow/Enter/Escape rules, the ↵ affordance and clearSearch(). The 220ms
|
||||||
|
// debounce is NOT here — it lives in Rust (search_qt::live), so the panel
|
||||||
|
// opens on the first keystroke instead of 220ms after the last one. The
|
||||||
|
// dropdown itself is shell/Cortinilla.qml.
|
||||||
// Right: the tri-state offline status badge with its flyout (recovery
|
// Right: the tri-state offline status badge with its flyout (recovery
|
||||||
// "Sign in" wired to QbzSession.recoveryLogin) and the app menu (user block
|
// "Sign in" wired to QbzSession.recoveryLogin) and the app menu (user block
|
||||||
// + Keyboard Shortcuts + Documentation + Log Out + Close. Still missing vs
|
// + Keyboard Shortcuts + Documentation + Log Out + Close. Still missing vs
|
||||||
@@ -665,24 +669,21 @@ Rectangle {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
horizontalAlignment: text === "" && !activeFocus ? Text.AlignHCenter : Text.AlignLeft
|
horizontalAlignment: text === "" && !activeFocus ? Text.AlignHCenter : Text.AlignLeft
|
||||||
clip: true
|
clip: true
|
||||||
|
// No QML debounce: the 220ms CORTINILLA_DEBOUNCE lives in Rust
|
||||||
|
// now (search_qt::live). Debouncing the whole invokable here
|
||||||
|
// meant the panel and its skeleton only appeared 220ms after the
|
||||||
|
// LAST keystroke, so continuous typing showed nothing at all;
|
||||||
|
// the reference opens on the FIRST keystroke >= 2 chars and
|
||||||
|
// debounces only the LOAD. Rust's version guard is what discards
|
||||||
|
// the superseded loads.
|
||||||
onTextEdited: {
|
onTextEdited: {
|
||||||
if (text.trim().length < 2) {
|
if (text.trim().length < 2) {
|
||||||
liveDebounce.stop()
|
|
||||||
QbzSearch.cortinillaDismiss()
|
QbzSearch.cortinillaDismiss()
|
||||||
} else {
|
} else {
|
||||||
liveDebounce.restart()
|
QbzSearch.searchLive(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 220ms live debounce (CORTINILLA_DEBOUNCE — one load per pause,
|
|
||||||
// not one per keystroke).
|
|
||||||
Timer {
|
|
||||||
id: liveDebounce
|
|
||||||
interval: 220
|
|
||||||
repeat: false
|
|
||||||
onTriggered: QbzSearch.searchLive(searchInput.text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Enter rule (HeaderBar.slint on-enter): cortinilla open +
|
// The Enter rule (HeaderBar.slint on-enter): cortinilla open +
|
||||||
// a keyboard selection -> activate the row; open + none -> full
|
// a keyboard selection -> activate the row; open + none -> full
|
||||||
// search; closed -> plain submit (also Search > All).
|
// search; closed -> plain submit (also Search > All).
|
||||||
|
|||||||
@@ -706,9 +706,24 @@ fn set_selected(index: i32, scroll_y: f64) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The live header query changed (QML debounced at 220ms, >= 2 chars —
|
/// The live header query changed — called on EVERY keystroke (>= 2 chars),
|
||||||
/// mirrors the Slint CORTINILLA_DEBOUNCE + the chars().count() gate).
|
/// not on a QML timer.
|
||||||
/// Opens the dropdown, resets the keyboard selection, and loads.
|
///
|
||||||
|
/// Two halves, and the split is load-bearing:
|
||||||
|
///
|
||||||
|
/// - **Synchronous half** — everything up to the sleep below. It runs on the
|
||||||
|
/// first poll of the spawned task, i.e. at the keystroke: gates, selection
|
||||||
|
/// reset, open, version bump, loading flag. This is what makes the panel and
|
||||||
|
/// its skeleton appear while the user is still typing, which is the
|
||||||
|
/// reference's behaviour (`qbz/src/main.rs:9641-9648` opens and raises
|
||||||
|
/// before starting the timer at `:9673`).
|
||||||
|
/// - **Debounced half** — after a 220 ms sleep (CORTINILLA_DEBOUNCE). A newer
|
||||||
|
/// keystroke has bumped the version by then, so the superseded task exits
|
||||||
|
/// without loading. Same shape as the immersive arm (`imm_live`).
|
||||||
|
///
|
||||||
|
/// `cortinillaLoading` has exactly ONE owner in the synchronous half and is
|
||||||
|
/// written exactly once more at the publish. Never in both halves for the
|
||||||
|
/// same keystroke.
|
||||||
pub async fn live(runtime: &Arc<AppRuntime<LoggingAdapter>>, query: &str) {
|
pub async fn live(runtime: &Arc<AppRuntime<LoggingAdapter>>, query: &str) {
|
||||||
let q = query.trim().to_string();
|
let q = query.trim().to_string();
|
||||||
if q.chars().count() < 2 {
|
if q.chars().count() < 2 {
|
||||||
@@ -731,6 +746,16 @@ pub async fn live(runtime: &Arc<AppRuntime<LoggingAdapter>>, query: &str) {
|
|||||||
b.as_mut().set_cortinilla_loading(true);
|
b.as_mut().set_cortinilla_loading(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- end of the synchronous half; the debounce starts here ----------
|
||||||
|
// 220 ms (CORTINILLA_DEBOUNCE): one load per pause, not one per
|
||||||
|
// keystroke. A newer keystroke bumps CORT_VERSION while this sleeps, so
|
||||||
|
// the superseded task wakes, fails the guard and exits without touching
|
||||||
|
// the network — the same idiom `imm_live` uses.
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(220)).await;
|
||||||
|
if !is_current_cort_version(version) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let t = std::time::Instant::now();
|
let t = std::time::Instant::now();
|
||||||
// Blacklist filtering happens INSIDE search_all (`search.rs:1112-1115`).
|
// Blacklist filtering happens INSIDE search_all (`search.rs:1112-1115`).
|
||||||
let (bl, abl) = blacklist_snapshots();
|
let (bl, abl) = blacklist_snapshots();
|
||||||
@@ -805,6 +830,11 @@ pub async fn live(runtime: &Arc<AppRuntime<LoggingAdapter>>, query: &str) {
|
|||||||
/// Dismiss (Esc / click-outside / idle / page change).
|
/// Dismiss (Esc / click-outside / idle / page change).
|
||||||
pub fn dismiss() {
|
pub fn dismiss() {
|
||||||
set_cortinilla_open(false);
|
set_cortinilla_open(false);
|
||||||
|
// Dismiss is a CANCELLATION POINT. Without the bump an in-flight load
|
||||||
|
// survives the Esc and still publishes into a closed dropdown — and it
|
||||||
|
// also rewrites LAST_CORT, which used to be what Enter submitted. The
|
||||||
|
// bump makes every task in flight fail its version guard and exit.
|
||||||
|
next_cort_version();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Arrow-key move (delta -1 up / +1 down; the Slint on_cortinilla_move_selection
|
/// Arrow-key move (delta -1 up / +1 down; the Slint on_cortinilla_move_selection
|
||||||
|
|||||||
Reference in new issue
Block a user