fix(qs,fluxo) performance fix for repaints
This commit is contained in:
@@ -22,6 +22,11 @@ Item {
|
||||
readonly property string incomingTitle: mine && toplevel.title ? toplevel.title : "—"
|
||||
property string displayedTitle: "—"
|
||||
|
||||
// Which toplevel the landing transition last played for. The landing is a
|
||||
// focus-change stamp, so it has to key off window identity rather than off
|
||||
// the caption text — see commitTitle().
|
||||
property var landedToplevel: null
|
||||
|
||||
// The bar shrinks this when the three clusters would otherwise collide.
|
||||
property real maxWidth: 240
|
||||
|
||||
@@ -67,11 +72,28 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Adopt the settled caption, and stamp it only when focus actually moved.
|
||||
//
|
||||
// The landing used to restart on any title change, which is not the same
|
||||
// thing: plenty of windows rewrite their own title on a timer — a terminal
|
||||
// running a task with a spinner or a percentage, a browser tab with a live
|
||||
// clock — and each rewrite restarted seven overlapping animations totalling
|
||||
// over half a second. At a title churning twice a second on a 240 Hz screen
|
||||
// the bar never stopped animating, and repainting it that hard measured at
|
||||
// roughly 18% of a CPU core with nothing else happening. Keying the stamp to
|
||||
// window identity restores what the effect was described as doing.
|
||||
function commitTitle(): void {
|
||||
if (root.displayedTitle === root.incomingTitle)
|
||||
const focusMoved = root.toplevel !== root.landedToplevel;
|
||||
|
||||
if (root.displayedTitle !== root.incomingTitle)
|
||||
root.displayedTitle = root.incomingTitle;
|
||||
else if (!focusMoved)
|
||||
return;
|
||||
root.displayedTitle = root.incomingTitle;
|
||||
titleLanding.restart();
|
||||
|
||||
if (focusMoved) {
|
||||
root.landedToplevel = root.toplevel;
|
||||
titleLanding.restart();
|
||||
}
|
||||
}
|
||||
|
||||
// Browser tabs, terminals and editors can update titles several times in a
|
||||
@@ -85,7 +107,17 @@ Item {
|
||||
}
|
||||
|
||||
onIncomingTitleChanged: titleSettle.restart()
|
||||
Component.onCompleted: displayedTitle = incomingTitle
|
||||
|
||||
// Focus can move to a window whose caption happens to match the outgoing
|
||||
// one, which changes no title and so would otherwise never land.
|
||||
onToplevelChanged: titleSettle.restart()
|
||||
|
||||
Component.onCompleted: {
|
||||
displayedTitle = incomingTitle;
|
||||
// Adopt the current window silently, so a config reload does not play a
|
||||
// focus-change stamp for a focus that did not change.
|
||||
landedToplevel = toplevel;
|
||||
}
|
||||
|
||||
// A quick crimson wipe under the title on focus change.
|
||||
Rectangle {
|
||||
|
||||
@@ -109,15 +109,20 @@ Item {
|
||||
|
||||
// Track progress, shown until the pointer arrives and the hover rule
|
||||
// takes the band over.
|
||||
//
|
||||
// Deliberately untweened. MPRIS position is polled once a second, and a
|
||||
// 480 ms ease on a 1 Hz input meant this rule was mid-animation roughly
|
||||
// half of every second — about 115 animated frames per second on a
|
||||
// 240 Hz screen, each one rebuilding this Skew's Shape geometry through
|
||||
// the curve renderer and re-rendering the pill's layer textures with it.
|
||||
// Stepping once per second costs 1 frame instead of 115, matches the rate
|
||||
// the underlying data actually arrives at, and makes a seek land exactly
|
||||
// where it was dropped instead of gliding there.
|
||||
Skew {
|
||||
height: parent.height
|
||||
width: parent.width * Math.max(0, Math.min(1, root.progress))
|
||||
color: Theme.accent
|
||||
visible: root.progress >= 0 && !root.hovered && !root.active
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 480; easing.type: Easing.OutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
Skew {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import QtQuick.Window
|
||||
import Quickshell
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
@@ -126,35 +127,77 @@ BarPill {
|
||||
|
||||
// Compact play-state equaliser. It moves only while audio is playing
|
||||
// and freezes into three quiet bars when paused.
|
||||
//
|
||||
// Sampled from a timer rather than tweened by an infinite
|
||||
// NumberAnimation. DP-1 runs at 240 Hz, and a frame-synced animation
|
||||
// here repainted the entire bar 240 times a second: this pill sits
|
||||
// inside ancestors that use layer.enabled together with
|
||||
// Shape.CurveRenderer, and the album art carries a MultiEffect pass, so
|
||||
// every one of those frames re-rendered several layer textures. Measured
|
||||
// on its own, this one animation cost about two thirds of a CPU core
|
||||
// whenever anything was playing.
|
||||
//
|
||||
// The timer samples a sine, it does not step through arbitrary levels: a
|
||||
// low tick rate only looks like the original if consecutive ticks stay
|
||||
// near each other. Each bar keeps the eased breathing motion and the
|
||||
// period it had before — the curve is just read 25 times a second
|
||||
// instead of 240.
|
||||
Row {
|
||||
id: eq
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
// Sampling interval — the one knob trading smoothness against cost.
|
||||
// Frames are the whole expense: a full-bar repaint runs about 0.3% of
|
||||
// a core (the bar spans 2544x40 on a 240 Hz output), so cost scales
|
||||
// linearly with this rate. 16 ms holds 60 Hz, which is visually
|
||||
// indistinguishable from the original frame-synced tween at a quarter
|
||||
// of its ~75%.
|
||||
readonly property int tickMs: 16
|
||||
property int phase: 0
|
||||
|
||||
// Reproduces the original per-bar tween exactly, sampled instead of
|
||||
// frame-synced. Each bar eased between two heights with its own pair
|
||||
// of durations, so the three differed in both swing and period — the
|
||||
// middle bar moved barely a pixel while the outer two ran in
|
||||
// opposition. Giving all three the full swing, as a single symmetric
|
||||
// sine would, is three times the motion and reads far busier than
|
||||
// this meter is supposed to.
|
||||
function level(i: int): real {
|
||||
const lo = 4 + (i * 5) % 13; // old first NumberAnimation `to`
|
||||
const hi = 14 - (i * 4) % 9; // old second NumberAnimation `to`
|
||||
const d1 = 320 + i * 90; // ...and their durations
|
||||
const d2 = 280 + i * 70;
|
||||
|
||||
const t = (eq.phase * eq.tickMs) % (d1 + d2);
|
||||
// Easing.InOutSine, which is what both halves used.
|
||||
const ease = x => (1 - Math.cos(Math.PI * x)) / 2;
|
||||
|
||||
return t < d1
|
||||
? hi + (lo - hi) * ease(t / d1)
|
||||
: lo + (hi - lo) * ease((t - d1) / d2);
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: eq.tickMs
|
||||
running: Media.playing && eq.visible && (eq.Window.window?.visible ?? true)
|
||||
repeat: true
|
||||
onTriggered: eq.phase++
|
||||
// Restart the cycle from its trough, so playback always begins
|
||||
// from a settled meter rather than mid-swing.
|
||||
onRunningChanged: if (!running) eq.phase = 0
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: 3
|
||||
|
||||
Rectangle {
|
||||
required property int index
|
||||
width: 3
|
||||
height: 6
|
||||
height: Media.playing ? eq.level(index) : 6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Media.playing ? Theme.primary : Theme.muted
|
||||
|
||||
SequentialAnimation on height {
|
||||
running: Media.playing
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 4 + (index * 5) % 13
|
||||
duration: 320 + index * 90
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 14 - (index * 4) % 9
|
||||
duration: 280 + index * 70
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durBase }
|
||||
}
|
||||
|
||||
@@ -132,8 +132,13 @@ Item {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
// Hyprland evaluates IPC `dispatch` payloads as Lua now that the
|
||||
// config is Lua (hyprland.lua rather than hyprland.conf), so the
|
||||
// old `dispatch workspace 3` string is a Lua syntax error and the
|
||||
// click silently does nothing. Dispatchers have to be called the
|
||||
// same way lua/binds/workspaces.lua calls them.
|
||||
TapHandler {
|
||||
onTapped: Hyprland.dispatch("workspace " + pip.wsId)
|
||||
onTapped: Hyprland.dispatch("hl.dsp.focus({workspace = " + pip.wsId + "})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user