feat(qt): reduceMotion seam and a coarse skeleton tick
The owner ruled 3-C: build the seam here so the kiosk port consumes it instead of having to invent it. The earlier plan was to wait for kiosk to define it, but kiosk is not implemented - it is a contract - so "wait for kiosk" meant waiting on a date that does not exist. The shape is no longer a guess, which was the only objection to building it early. Two independent sources fix it: the kiosk contract mandates "FORCED reduce-motion while in kiosk", and the reference computes the flag as kiosk_profile || !use_gpu_renderer. QbzShell.reduceMotion is derived, never a preference - do not add a Settings row for it. The existing play-indicator toggle is a preference and a different thing; binding them would make "animate the play indicator" change this dropdown's pulse rate. Only the kiosk half is wired, and the omission is deliberate rather than partial: the renderer tier has no honest source here. It is a QML-side fact, this port runs on the GPU in every real session, and the one place a probe would report software is the offscreen smoke, which forces it by definition. Wiring it would turn reduce-motion on exactly where it means nothing. The kiosk literal is a named const with a pointer to their contract; flipping it is one line on their side. The cortinilla skeleton is the ONE consumer: its single host Timer drops from 700ms to 2000ms. No new timer, no per-delegate animation - the reference's own note records that this pulse was the main cost of opening the cortinilla on weak GPUs. Giving that Column an id also fixes a real scope bug on the way past. The Timer read an unqualified `parent`, and Timer derives from QtObject, which has no parent property - so it resolved up the scope chain, out of the block entirely. The sibling rectangles' parent.parent[.parent] chains are now explicit too. EqualizerBars.qml and the immersive contract's D11 both recorded that Qt had no reduce-motion twin. Both are corrected: the seam exists, those surfaces are simply not consumers yet, and wiring each is its own one-liner. Contract: qbz-nix-docs/qt-frontend/2026-08-03-cortinilla-parity, commit C4b (section 5.g G1-G5, ruling R10; D-BUG-1 folded in). Gate: build clean, 258/258 tests, both QML audits OK, offscreen smoke 0 complaints + home published. AC-39: exactly one behavioural consumer. cc
This commit is contained in:
3 files changed
+58
-7
No files matched your search
@@ -16,7 +16,10 @@
|
||||
// (Svelte keyframes 0/100% -> 0.3, 50% -> 1.0). The loop runs only while
|
||||
// the item is visible — every mount site gates visibility on npPlaying
|
||||
// (ImmersiveTrackInfo.slint:85,:100), so a paused player animates nothing.
|
||||
// Slint's reduce-motion coarse clock has NO Qt twin (D11) — not ported.
|
||||
// Reduce-motion: the seam EXISTS now — `QbzShell.reduceMotion`, built by the
|
||||
// cortinilla-parity contract (§5.g). This surface is simply not a consumer
|
||||
// yet; wiring it is a one-liner (throttle the tick when it is true) and its
|
||||
// own change, not a side effect of someone else's commit.
|
||||
|
||||
import QtQuick
|
||||
|
||||
|
||||
@@ -193,15 +193,24 @@ Item {
|
||||
// --- Loading skeleton (no cached instant-paint — one clean
|
||||
// apply, Cortinilla.slint) -------------------------------
|
||||
Column {
|
||||
id: skeleton
|
||||
visible: QbzSearch.cortinillaLoading
|
||||
width: parent.width
|
||||
topPadding: 4
|
||||
property bool pulse: false
|
||||
// ONE host Timer flipping ONE bool for all five rows —
|
||||
// never a per-delegate animation. Under reduce-motion it
|
||||
// drops to a ~8fps coarse tick, which is what the
|
||||
// reference switches its pulse's time source to
|
||||
// (Cortinilla.slint:128-131). That file records why: this
|
||||
// pulse was the main cost of opening the cortinilla on
|
||||
// weak GPUs, because every frame was a full-window
|
||||
// repaint.
|
||||
Timer {
|
||||
interval: 700
|
||||
running: parent.visible
|
||||
interval: QbzShell.reduceMotion ? 2000 : 700
|
||||
running: skeleton.visible
|
||||
repeat: true
|
||||
onTriggered: parent.pulse = !parent.pulse
|
||||
onTriggered: skeleton.pulse = !skeleton.pulse
|
||||
}
|
||||
Repeater {
|
||||
model: 5
|
||||
@@ -215,7 +224,7 @@ Item {
|
||||
height: 40
|
||||
radius: 4
|
||||
color: theme.surfaceElevated
|
||||
opacity: parent.parent.pulse ? 0.48 : 0.16
|
||||
opacity: skeleton.pulse ? 0.48 : 0.16
|
||||
Behavior on opacity { NumberAnimation { duration: 650; easing.type: Easing.InOutQuad } }
|
||||
}
|
||||
Column {
|
||||
@@ -227,7 +236,7 @@ Item {
|
||||
height: 11
|
||||
radius: 3
|
||||
color: theme.surfaceElevated
|
||||
opacity: parent.parent.parent.pulse ? 0.48 : 0.16
|
||||
opacity: skeleton.pulse ? 0.48 : 0.16
|
||||
Behavior on opacity { NumberAnimation { duration: 650; easing.type: Easing.InOutQuad } }
|
||||
}
|
||||
Rectangle {
|
||||
@@ -235,7 +244,7 @@ Item {
|
||||
height: 9
|
||||
radius: 3
|
||||
color: theme.surfaceElevated
|
||||
opacity: parent.parent.parent.pulse ? 0.48 : 0.16
|
||||
opacity: skeleton.pulse ? 0.48 : 0.16
|
||||
Behavior on opacity { NumberAnimation { duration: 650; easing.type: Easing.InOutQuad } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,19 @@ pub mod qbz_shell {
|
||||
// "ambient"). Live — toggling applies immediately (pure QML
|
||||
// layering). Combined in QML with npHasTrack for the D4 no-track
|
||||
// rule.
|
||||
/// Reduce-motion: throttle every continuous animation to a coarse
|
||||
/// tick instead of the display rate.
|
||||
///
|
||||
/// DERIVED, never a user preference — the reference computes it as
|
||||
/// `kiosk_profile || !use_gpu_renderer`
|
||||
/// (`qbz-nix/crates/qbz/src/main.rs:8597`) and there is no Settings
|
||||
/// row for it in either frontend. Do not add one: the play-indicator
|
||||
/// animation toggle is a *preference* and is a different thing.
|
||||
///
|
||||
/// Built by the cortinilla-parity contract (§5.g, ruling R10) so the
|
||||
/// KIOSK port can consume it rather than having to invent it. See
|
||||
/// `set_reduce_motion` for the two halves and why one is dormant.
|
||||
#[qproperty(bool, reduce_motion)]
|
||||
#[qproperty(i32, ambient_mode)]
|
||||
// Album-art triad (ambient_qt.rs), pushed on track change.
|
||||
#[qproperty(QString, ambient_primary)]
|
||||
@@ -399,6 +412,7 @@ pub struct QbzShellRust {
|
||||
window_maximized: bool,
|
||||
window_min_width: f32,
|
||||
window_min_height: f32,
|
||||
reduce_motion: bool,
|
||||
ambient_mode: i32,
|
||||
ambient_primary: QString,
|
||||
ambient_secondary: QString,
|
||||
@@ -460,6 +474,7 @@ impl Default for QbzShellRust {
|
||||
window_maximized: crate::settings_qt::window_maximized(),
|
||||
window_min_width: crate::settings_qt::WINDOW_MIN_WIDTH,
|
||||
window_min_height: crate::settings_qt::WINDOW_MIN_HEIGHT,
|
||||
reduce_motion: reduce_motion_at_boot(),
|
||||
ambient_mode: crate::settings_qt::app_background_mode(),
|
||||
// The Slint ImmersiveState default triad (pre-artwork colors).
|
||||
ambient_primary: QString::from("#00dcc8"),
|
||||
@@ -526,6 +541,30 @@ static QT_THREAD: OnceLock<CxxQtThread<QbzShell>> = OnceLock::new();
|
||||
|
||||
/// Queue a shell-bridge mutation onto the Qt event loop (no-op before
|
||||
/// boot registers the thread).
|
||||
/// Kiosk profile — NOT PORTED YET. The kiosk contract
|
||||
/// (`qbz-nix-docs/qt-frontend/2026-08-02-kiosk-port/00-CONTRACT.md:244`)
|
||||
/// mandates "FORCED reduce-motion while in kiosk", so when that lands it
|
||||
/// replaces this literal with the real profile flag and the seam is already
|
||||
/// tested. One line, on their side.
|
||||
const KIOSK_PROFILE_ACTIVE: bool = false;
|
||||
|
||||
/// The boot value of [`reduce_motion`].
|
||||
///
|
||||
/// The reference is `kiosk_profile || !use_gpu_renderer`. Only the FIRST half
|
||||
/// is wired here, deliberately:
|
||||
///
|
||||
/// The renderer-tier half has no honest source in this port. The tier is a
|
||||
/// QML-side fact (`GraphicsInfo.api`), not a Rust one, and this port runs on
|
||||
/// the GPU in every real session (OpenGL RHI, measured 2026-07-29). The one
|
||||
/// place where a tier probe WOULD report software is the offscreen smoke,
|
||||
/// which forces it by definition — so wiring it would turn reduce-motion on
|
||||
/// exactly where it means nothing and stay off everywhere it would matter.
|
||||
/// If a genuine software-tier session ever appears (a remote desktop, a
|
||||
/// llvmpipe box), that is when the second half earns its probe.
|
||||
fn reduce_motion_at_boot() -> bool {
|
||||
KIOSK_PROFILE_ACTIVE
|
||||
}
|
||||
|
||||
pub(crate) fn ui(f: impl FnOnce(Pin<&mut QbzShell>) + Send + 'static) {
|
||||
if let Some(thread) = QT_THREAD.get() {
|
||||
let _ = thread.queue(f);
|
||||
|
||||
Reference in new issue
Block a user