diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml index ea5fe9ef..c1b62f3b 100644 --- a/.github/workflows/check-format.yml +++ b/.github/workflows/check-format.yml @@ -18,6 +18,7 @@ jobs: - name: Check QML format shell: fish {0} run: | + /usr/lib/qt6/bin/qmlformat --version for file in (string match -v 'build/*' **.qml) /usr/lib/qt6/bin/qmlformat $file | diff -u $file - || exit 1 end @@ -34,5 +35,6 @@ jobs: - name: Check C++ format shell: fish {0} run: | + clang-format --version find plugin extras -name '*.cpp' -o -name '*.hpp' \ | xargs clang-format --dry-run --Werror diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7962963c..9dd0d576 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,6 +25,8 @@ jobs: - name: Lint QML shell: fish {0} run: | + /usr/lib/qt6/bin/qmllint --version + # Generate tooling touch .qmlls.ini QT_QPA_PLATFORM=offscreen QML2_IMPORT_PATH="$PWD/build/qml:$QML2_IMPORT_PATH" timeout 2 qs -p . diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml index 74fe5435..34b9a6fc 100644 --- a/.github/workflows/update-image.yml +++ b/.github/workflows/update-image.yml @@ -23,7 +23,13 @@ jobs: run: | cat > /tmp/Dockerfile < /etc/pacman.d/mirrorlist && \ + pacman -Syyuu --needed --noconfirm sudo base-devel cmake ninja fish git clazy qt6-declarative qt6-shadertools python libpipewire aubio libqalculate && \ useradd -m builder && \ echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \ sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index f62d0b98..1cd9243e 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -10,25 +10,25 @@ import Caelestia.Config Item { id: model - property alias visibleModel: _visibleModel + property alias visibleModel: visibleModel property string activeLabel: "" property int activeIndex: -1 property var _xkbMap: ({}) property bool _notifiedLimit: false function start() { - _xkbXmlBase.running = true; - _getKbLayoutOpt.running = true; + xkbXmlBase.running = true; + getKbLayoutOpt.running = true; } function refresh() { _notifiedLimit = false; - _getKbLayoutOpt.running = true; + getKbLayoutOpt.running = true; } function switchTo(idx) { - _switchProc.command = ["hyprctl", "switchxkblayout", "all", String(idx)]; - _switchProc.running = true; + switchProc.command = ["hyprctl", "switchxkblayout", "all", String(idx)]; + switchProc.running = true; } function _buildXmlMap(xml) { @@ -50,19 +50,19 @@ Item { _xkbMap = map; - if (_layoutsModel.count > 0) { + if (layoutsModel.count > 0) { const tmp = []; - for (let i = 0; i < _layoutsModel.count; i++) { - const it = _layoutsModel.get(i); + for (let i = 0; i < layoutsModel.count; i++) { + const it = layoutsModel.get(i); tmp.push({ layoutIndex: it.layoutIndex, token: it.token, label: _pretty(it.token) }); } - _layoutsModel.clear(); - tmp.forEach(t => _layoutsModel.append(t)); - _fetchActiveLayouts.running = true; + layoutsModel.clear(); + tmp.forEach(t => layoutsModel.append(t)); + fetchActiveLayouts.running = true; } } @@ -78,7 +78,7 @@ Item { function _setLayouts(raw) { const parts = raw.split(",").map(s => s.trim()).filter(Boolean); - _layoutsModel.clear(); + layoutsModel.clear(); const seen = new Set(); let idx = 0; @@ -87,7 +87,7 @@ Item { if (seen.has(p)) continue; seen.add(p); - _layoutsModel.append({ + layoutsModel.append({ layoutIndex: idx, token: p, label: _pretty(p) @@ -97,19 +97,19 @@ Item { } function _rebuildVisible() { - _visibleModel.clear(); + visibleModel.clear(); let arr = []; - for (let i = 0; i < _layoutsModel.count; i++) - arr.push(_layoutsModel.get(i)); + for (let i = 0; i < layoutsModel.count; i++) + arr.push(layoutsModel.get(i)); arr = arr.filter(i => i.layoutIndex !== activeIndex); - arr.forEach(i => _visibleModel.append(i)); + arr.forEach(i => visibleModel.append(i)); if (!GlobalConfig.utilities.toasts.kbLimit) return; - if (_layoutsModel.count > 4) { + if (layoutsModel.count > 4) { Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); } } @@ -124,26 +124,26 @@ Item { visible: false ListModel { - id: _visibleModel + id: visibleModel } ListModel { - id: _layoutsModel + id: layoutsModel } Process { - id: _xkbXmlBase + id: xkbXmlBase command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] stdout: StdioCollector { onStreamFinished: model._buildXmlMap(text) } - onRunningChanged: if (!running && (typeof _xkbXmlBase.exitCode !== "undefined") && _xkbXmlBase.exitCode !== 0) // qmllint disable missing-property - _xkbXmlEvdev.running = true + onRunningChanged: if (!running && (typeof xkbXmlBase.exitCode !== "undefined") && xkbXmlBase.exitCode !== 0) // qmllint disable missing-property + xkbXmlEvdev.running = true } Process { - id: _xkbXmlEvdev + id: xkbXmlEvdev command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] stdout: StdioCollector { @@ -152,7 +152,7 @@ Item { } Process { - id: _getKbLayoutOpt + id: getKbLayoutOpt command: ["hyprctl", "-j", "getoption", "input:kb_layout"] stdout: StdioCollector { @@ -162,17 +162,17 @@ Item { const raw = (j?.str || j?.value || "").toString().trim(); if (raw.length) { model._setLayouts(raw); - _fetchActiveLayouts.running = true; + fetchActiveLayouts.running = true; return; } } catch (e) {} - _fetchLayoutsFromDevices.running = true; + fetchLayoutsFromDevices.running = true; } } } Process { - id: _fetchLayoutsFromDevices + id: fetchLayoutsFromDevices command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { @@ -184,13 +184,13 @@ Item { if (raw.length) model._setLayouts(raw); } catch (e) {} - _fetchActiveLayouts.running = true; + fetchActiveLayouts.running = true; } } } Process { - id: _fetchActiveLayouts + id: fetchActiveLayouts command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { @@ -201,7 +201,7 @@ Item { const idx = kb?.active_layout_index ?? -1; model.activeIndex = idx >= 0 ? idx : -1; - model.activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; + model.activeLabel = (idx >= 0 && idx < layoutsModel.count) ? layoutsModel.get(idx).label : ""; } catch (e) { model.activeIndex = -1; model.activeLabel = ""; @@ -213,9 +213,9 @@ Item { } Process { - id: _switchProc + id: switchProc onRunningChanged: if (!running) - _fetchActiveLayouts.running = true + fetchActiveLayouts.running = true } } diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index 8eed3653..61181819 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -32,6 +32,18 @@ static float cpuSmoothstep(float edge0, float edge1, float x) { return t * t * (3.0f - 2.0f * t); } +static float cornerFillFactor(float sd, float smoothFactor) { + // Continuous two-sided window. The corner is squared (factor -> 0) only within + // ±smoothFactor of the neighbour's edge (the visible junction); it keeps its full + // radius both far outside the neighbour and deep inside it (where it is buried and + // squaring would only crease the interior). C0-continuous across sd = 0 — unlike the + // old `if (sd >= 0)` branch, which snapped the radius full<->square (factor 1<->0) on + // sub-pixel motion as a corner crossed the edge, flickering the fill bridge in/out. + const float outside = cpuSmoothstep(0.0f, smoothFactor, sd); // 0 at edge, ->1 far outside + const float inside = cpuSmoothstep(0.0f, -smoothFactor, sd); // 0 at edge, ->1 deep inside + return std::max(outside, inside); +} + BlobShape::BlobShape(QQuickItem* parent) : QQuickItem(parent) { setFlag(ItemHasContents); @@ -310,20 +322,16 @@ void BlobShape::updatePolish() { if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si)) continue; const auto& rj = m_cachedRects[j]; - // Skip when the corner is inside rj: it's buried, not on a visible junction, - // so squaring it would only perturb interior SDF gradients. + // Square each corner only near rj's edge; keep full radius far outside AND + // deep inside rj (buried, so it can't crease the visible junction). const float sdTr = cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh); const float sdBr = cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh); const float sdBl = cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh); const float sdTl = cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh); - if (sdTr >= 0.0f) - fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, sdTr)); - if (sdBr >= 0.0f) - fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, sdBr)); - if (sdBl >= 0.0f) - fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, sdBl)); - if (sdTl >= 0.0f) - fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, sdTl)); + fTr = std::min(fTr, cornerFillFactor(sdTr, smoothFactor)); + fBr = std::min(fBr, cornerFillFactor(sdBr, smoothFactor)); + fBl = std::min(fBl, cornerFillFactor(sdBl, smoothFactor)); + fTl = std::min(fTl, cornerFillFactor(sdTl, smoothFactor)); } if (cornerFill && m_cachedHasInverted) { diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index 36ddaaa4..138928fa 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -42,9 +42,12 @@ float sdBox(vec2 p, vec2 center, vec2 halfSize) { float smin(float a, float b, float k) { // Circular smooth min — the blend fillet is a true circular arc of radius k, - // tangent to both surfaces (not a polynomial/squircle curve). Deviation region - // width = k (deviates only where both a < k and b < k), matching the cubic it - // replaces. Always <= min(a, b); max blend depth at a == b is (sqrt(2) - 1) * k. + // tangent to both surfaces (not a polynomial/squircle curve). Deviates from + // min(a, b) only in the corner region where BOTH a < k and b < k (unlike the + // cubic it replaced, which deviated over the whole band |a - b| < k). Always + // <= min(a, b); max blend depth at a == b is (sqrt(2) - 1) * k. It is C1 but + // not C2 at the support boundary, so a circular-arc fillet shows the usual + // line-meets-arc curvature step — by design, that is the "circular" look. return max(k, min(a, b)) - length(max(vec2(k) - vec2(a, b), vec2(0.0))); } @@ -164,8 +167,8 @@ void main() { continue; if ((excludeMask & (1 << j)) != 0) continue; - // smin only deviates from min within smoothFactor - if (abs(dArr[i] - dArr[j]) >= smoothFactor) + // Circular smin deviates from min only where BOTH dArr are < smoothFactor. + if (max(dArr[i], dArr[j]) >= smoothFactor) continue; mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor)); } @@ -194,8 +197,13 @@ void main() { // Screen-space center (with offset) and pre-computed AABB half-extents vec2 ctr = rect.xy + sinkProps.yz; - // Delay sink to absorb smin blend depth (circular smin max = (sqrt(2)-1)*k) - float preOff = smoothFactor * (sqrt(2.0) - 1.0); + // Sink onset / residual overlap: how far a rect must penetrate the border before + // the inner wall recedes to form its pocket. Too low and the wall recedes faster + // than the junction can stay convex, denting the inner edge inward near the rect's + // (squared) corners; too high and the rect nestles too deep before the wall yields. + // Tuned between the old cubic blend depth (k/6, too shallow) and the circular blend + // depth ((sqrt2-1)k): half the circular smin gap-closing distance, (2-sqrt2)k/2. + float preOff = smoothFactor * (2.0 - sqrt(2.0)) * 0.5; // Top border: track rect's BOTTOM edge, only within border thickness float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, innerTop - outerTop); @@ -231,7 +239,16 @@ void main() { dInner -= sinkValue; - float dFrame = smaxSharpA(dOuter, -dInner, smoothFactor); + // The circular smax fillet has radius kFrame; when it exceeds the border thickness + // it can't complete inside the border, so the sharp outer-box term bleeds onto the + // inner edge and bulges the inner corners (worst when thickness < smoothFactor — the + // default border is thinner than the blend radius). Clamp kFrame to the thinnest side + // so the inner edge stays a clean constant-radius arc. Each inner corner is bounded by + // its thinner adjacent side, so the global min is correct for every corner. + float minThick = min(min(innerTop - outerTop, outerBot - innerBot), + min(innerLeft - outerLeft, outerRight - innerRight)); + float kFrame = clamp(min(smoothFactor, minThick - 1.0), 1.0, smoothFactor); + float dFrame = smaxSharpA(dOuter, -dInner, kFrame); mergedSdf = smin(mergedSdf, dFrame, smoothFactor); if (dFrame < minDist) { diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml index 6c4dc8d2..c3299a12 100644 --- a/services/NetworkUsage.qml +++ b/services/NetworkUsage.qml @@ -20,8 +20,8 @@ Singleton { readonly property real uploadTotal: _uploadTotal // History buffers for sparkline - readonly property CircularBuffer downloadBuffer: _downloadBuffer - readonly property CircularBuffer uploadBuffer: _uploadBuffer + readonly property alias downloadBuffer: downloadHistory + readonly property alias uploadBuffer: uploadHistory readonly property int historyLength: 30 // Private properties @@ -137,13 +137,13 @@ Singleton { } CircularBuffer { - id: _downloadBuffer + id: downloadHistory capacity: root.historyLength + 1 } CircularBuffer { - id: _uploadBuffer + id: uploadHistory capacity: root.historyLength + 1 } @@ -200,10 +200,10 @@ Singleton { root._uploadSpeed = txDelta / timeDelta; if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) - _downloadBuffer.push(root._downloadSpeed); + downloadHistory.push(root._downloadSpeed); if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) - _uploadBuffer.push(root._uploadSpeed); + uploadHistory.push(root._uploadSpeed); } // Calculate totals with overflow handling