feat(lyrics): lite-fill display pref — static active-line highlight to cut CPU

The per-word karaoke sweep animates a clip rect ~60x/s, driving continuous full-window repaints (~4% CPU). Lite fill highlights the active line whole (no per-frame animation) so the surface only repaints on line changes. New toggle in the lyrics controls flyout, persisted via lyrics_prefs; default off (smooth karaoke). Kiosk / weak-GPU tiers force it on via ShellState.reduce-motion.

cc
This commit is contained in:
vicrodh committed 2026-07-12 19:38:43 -06:00
1 parent 5fa4e8bdd5
commit ec8c487564
4 files changed
+44 -7

No files matched your search

@@ -176,6 +176,21 @@ export component LyricsControlsPanel inherits Rectangle {
}
}
// Lite fill (perf): highlight the active line whole instead of the
// per-word karaoke sweep — stops the continuous full-window repaints.
HorizontalLayout {
height: 22px;
spacing: 8px;
RowLabel { text: @tr("Lite fill (saves CPU)"); }
QbzToggle {
checked: LyricsState.lite-fill;
toggled(v) => {
LyricsState.lite-fill = v;
LyricsState.prefs-changed();
}
}
}
// Dimming (Tauri segmented Off/Soft/Strong -> select).
HorizontalLayout {
height: 30px;
+14 -7
View File
@@ -44,7 +44,7 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { LyricsState, LyricsSegment } from "../state.slint";
import { LyricsState, LyricsSegment, ShellState } from "../state.slint";
// Q6 (SHIPPED): Tauri's four non-system lyric font families, embedded into
// the binary at compile time (slint-build default EmbedAllResources for Rust
@@ -218,17 +218,24 @@ component LyricsLineRow inherits VerticalLayout {
// Bright (sung) overlay, clipped to this segment's LOCAL fill.
// local = clamp((line-progress - start-weight) / width-weight, 0, 1)
Rectangle {
// Lite fill (pref) or reduce-motion (kiosk / weak GPU): the
// active line is fully lit and STATIC — no per-frame clip
// animation, so the surface stops repainting the whole window
// ~60x/s just to sweep the karaoke fill.
property <bool> cheap: ShellState.reduce-motion || LyricsState.lite-fill;
x: 0;
y: 0;
width: segbase.width * Math.clamp(
seg.width-weight > 0
? (LyricsState.line-progress - seg.start-weight) / seg.width-weight
: 0,
0.0, 1.0);
width: self.cheap
? segbase.width
: segbase.width * Math.clamp(
seg.width-weight > 0
? (LyricsState.line-progress - seg.start-weight) / seg.width-weight
: 0,
0.0, 1.0);
height: segbase.height;
clip: true;
animate width {
duration: LyricsState.fill-anim-ms * 1ms;
duration: self.cheap ? 0ms : LyricsState.fill-anim-ms * 1ms;
easing: linear;
}
Text {
+6
View File
@@ -4220,6 +4220,12 @@ export global LyricsState {
// 0 off · 1 soft · 2 strong (Tauri default 'strong').
in-out property <int> dimming-mode: 2;
in-out property <bool> uppercase: false;
// Lite fill (perf): when true, the active line is highlighted whole instead
// of the per-word karaoke sweep — no per-frame clip animation, so the lyrics
// surface stops driving continuous full-window repaints (~4% CPU -> idle
// between line changes). Default false (smooth karaoke). Kiosk / weak-GPU
// tiers force it on via ShellState.reduce-motion regardless of this pref.
in-out property <bool> lite-fill: false;
// Active (karaoke fill) color. `use-custom-color: false` = follow the theme
// accent (Tauri activeColor: ''). The palette swatches set both (deviation
// D6: swatches instead of a native color input — Slint has none).
+9
View File
@@ -52,6 +52,10 @@ pub struct LyricsPrefs {
pub active_color: String, // "#RRGGBB" or "" = theme accent
#[serde(default)]
pub uppercase: bool,
/// Lite fill (perf): highlight the active line whole instead of the per-word
/// karaoke sweep, so the lyrics surface stops driving continuous repaints.
#[serde(default)]
pub lite_fill: bool,
}
fn d_true() -> bool {
@@ -77,6 +81,7 @@ impl Default for LyricsPrefs {
dimming: d_dimming(),
active_color: String::new(),
uppercase: false,
lite_fill: false,
}
}
}
@@ -212,6 +217,7 @@ pub fn apply_to_ui(window: &AppWindow, prefs: &LyricsPrefs) {
}
None => state.set_use_custom_color(false),
}
state.set_lite_fill(prefs.lite_fill);
}
/// Read the in-out props back into a pref set and persist — the
@@ -241,6 +247,7 @@ pub fn persist_from_ui(window: &AppWindow) {
String::new()
},
uppercase: state.get_uppercase(),
lite_fill: state.get_lite_fill(),
};
save(&prefs);
}
@@ -283,6 +290,7 @@ mod tests {
dimming: "max".into(),
active_color: "purple".into(),
uppercase: true,
lite_fill: false,
}
.sanitized();
assert!(!p.auto_follow); // bools pass through
@@ -302,6 +310,7 @@ mod tests {
dimming: "off".into(),
active_color: "#8b5cf6".into(),
uppercase: false,
lite_fill: true,
}
.sanitized();
assert_eq!(p.font, "line-seed-jp");