diff --git a/crates/qbz-qt/qml/shell/Cortinilla.qml b/crates/qbz-qt/qml/shell/Cortinilla.qml index b103ecf47..b7c53f603 100644 --- a/crates/qbz-qt/qml/shell/Cortinilla.qml +++ b/crates/qbz-qt/qml/shell/Cortinilla.qml @@ -19,7 +19,12 @@ import "../theme" Item { id: root anchors.fill: parent - visible: QbzSearch.cortinillaOpen + // Open AND the live query long enough to have meaningful results — the + // same >= 2 gate the controller applies, so a 1-char query never flashes + // a half-empty panel (Cortinilla.slint:190-191). cortinillaQuery is + // published synchronously per keystroke, so this tracks what the user + // has typed rather than what last finished loading. + visible: QbzSearch.cortinillaOpen && QbzSearch.cortinillaQuery.length >= 2 // The host HeaderBar (for clearSearch on row activation). property var headerBar: null @@ -206,7 +211,12 @@ Item { Text { x: 14 anchors.verticalCenter: parent.verticalCenter - text: QbzSession.tr("No results for", QbzSession.trRev) + " “" + (root.doc.query || "") + "”" + // The LIVE query, matching the reference + // (Cortinilla.slint:373 reads + // SearchState.cortinilla-query, not the payload's + // echo) — otherwise the empty state quotes the + // previous query while the user reads it. + text: QbzSession.tr("No results for", QbzSession.trRev) + " “" + QbzSearch.cortinillaQuery + "”" color: theme.textMuted font.pixelSize: 12 } diff --git a/crates/qbz-qt/src/main.rs b/crates/qbz-qt/src/main.rs index 6dcdf7e73..2551152e9 100644 --- a/crates/qbz-qt/src/main.rs +++ b/crates/qbz-qt/src/main.rs @@ -1774,6 +1774,10 @@ fn republish_artist(artist_id: String) { // ============================ Search (phase 15) =========================== pub(crate) fn search_live(query: String) { + // The live query is published FIRST, before the offline bail and before + // anything async: it is what Enter and "View more" submit, and it must + // describe what the user has typed, never what last finished loading. + search_bridge::set_cortinilla_query(query.trim().to_string()); if offline_fwd::engine().status().is_offline() { // Qobuz-only surface (the local cortinilla sections are not // ported — search_qt.rs POC-NOTE): nothing to show offline. diff --git a/crates/qbz-qt/src/search_bridge.rs b/crates/qbz-qt/src/search_bridge.rs index 4179dc60e..2a26b916f 100644 --- a/crates/qbz-qt/src/search_bridge.rs +++ b/crates/qbz-qt/src/search_bridge.rs @@ -119,7 +119,7 @@ pub mod qbz_search { } use core::pin::Pin; -use std::sync::OnceLock; +use std::sync::{Mutex, OnceLock}; use cxx_qt::CxxQtThread; use cxx_qt::Threading as _; @@ -155,6 +155,37 @@ impl Default for QbzSearchRust { } } +/// Rust-side mirror of the `cortinillaQuery` property. +/// +/// The two `async fn`s that need the live query (`search_qt::view_more` and +/// `::search_all_action`) run on tokio, where reading a Qt property is not +/// possible without a round trip. Both are written by ONE function +/// ([`set_cortinilla_query`]) so they cannot diverge — the same discipline +/// `search_qt.rs`'s `CORT_OPEN` mirror documents for `cortinillaOpen`. +static CORT_QUERY: Mutex = Mutex::new(String::new()); + +/// Publish the live header query — property AND mirror, in that order. +/// +/// Called SYNCHRONOUSLY from the invokable path, ahead of the debounce and +/// ahead of any fetch, so a query the user has typed is readable the instant +/// it exists. Slint's twin is `SearchState.cortinilla-query`, set at +/// `qbz/src/main.rs:9647` before its debounce starts. +pub(crate) fn set_cortinilla_query(q: String) { + *CORT_QUERY.lock().unwrap_or_else(|e| e.into_inner()) = q.clone(); + ui(move |mut b| { + b.as_mut().set_cortinilla_query(QString::from(q.as_str())); + }); +} + +/// The live header query as last published. Empty before the first keystroke +/// and after a clear. +pub(crate) fn cortinilla_query() -> String { + CORT_QUERY + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone() +} + static QT_THREAD: OnceLock> = OnceLock::new(); /// Queue a search-bridge mutation onto the Qt event loop. NO-OP before diff --git a/crates/qbz-qt/src/search_qt.rs b/crates/qbz-qt/src/search_qt.rs index 57e919939..f0be2a50b 100644 --- a/crates/qbz-qt/src/search_qt.rs +++ b/crates/qbz-qt/src/search_qt.rs @@ -916,14 +916,18 @@ pub async fn view_more(runtime: &Arc>, kind: &str) { "playlist" => 4, _ => 0, }; - let q = LAST_CORT.lock().unwrap().as_ref().map(|d| d.query.clone()).unwrap_or_default(); + // The LIVE query, not the last successfully loaded payload's: a fetch can + // still be in flight (and `dismiss` is not a cancellation point), so + // LAST_CORT lags whatever the user has actually typed. + let q = crate::search_bridge::cortinilla_query(); set_cortinilla_open(false); submit(runtime, &q, Some(tab)).await; } /// The Enter affordance with no keyboard selection: full search, All tab. pub async fn search_all_action(runtime: &Arc>) { - let q = LAST_CORT.lock().unwrap().as_ref().map(|d| d.query.clone()).unwrap_or_default(); + // See `view_more`: the live query, never LAST_CORT's. + let q = crate::search_bridge::cortinilla_query(); set_cortinilla_open(false); submit(runtime, &q, Some(0)).await; }