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 {
|
||||
target: QbzSearch
|
||||
// Activity restarts the countdown (keystroke or arrow move).
|
||||
function onCortinillaJsonChanged() { if (root.visible && !root.panelHovered) idleClose.restart() }
|
||||
// Activity restarts the countdown (keystroke or arrow move). The
|
||||
// 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() }
|
||||
// Keyboard scroll-into-view: the controller publishes the selected
|
||||
// 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
|
||||
// re-centers with a 220ms animation (HeaderBar.slint:569).
|
||||
// Center: the search field, absolutely centered on the window (VISUAL
|
||||
// replica — the cortinilla/live-search is out of scope; POC-NOTE).
|
||||
// Center: the search field, absolutely centered on the window. It owns the
|
||||
// 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
|
||||
// "Sign in" wired to QbzSession.recoveryLogin) and the app menu (user block
|
||||
// + Keyboard Shortcuts + Documentation + Log Out + Close. Still missing vs
|
||||
@@ -665,24 +669,21 @@ Rectangle {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: text === "" && !activeFocus ? Text.AlignHCenter : Text.AlignLeft
|
||||
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: {
|
||||
if (text.trim().length < 2) {
|
||||
liveDebounce.stop()
|
||||
QbzSearch.cortinillaDismiss()
|
||||
} 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 +
|
||||
// a keyboard selection -> activate the row; open + none -> full
|
||||
// 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 —
|
||||
/// mirrors the Slint CORTINILLA_DEBOUNCE + the chars().count() gate).
|
||||
/// Opens the dropdown, resets the keyboard selection, and loads.
|
||||
/// The live header query changed — called on EVERY keystroke (>= 2 chars),
|
||||
/// not on a QML timer.
|
||||
///
|
||||
/// 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) {
|
||||
let q = query.trim().to_string();
|
||||
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);
|
||||
});
|
||||
|
||||
// ---- 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();
|
||||
// Blacklist filtering happens INSIDE search_all (`search.rs:1112-1115`).
|
||||
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).
|
||||
pub fn dismiss() {
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user