Merge branch 'unstable' into feat/nexus

This commit is contained in:
2 * r + 2 * t 2026-06-04 19:32:29 +10:00
commit 34b506fda1
7 changed files with 94 additions and 59 deletions

View file

@ -18,6 +18,7 @@ jobs:
- name: Check QML format - name: Check QML format
shell: fish {0} shell: fish {0}
run: | run: |
/usr/lib/qt6/bin/qmlformat --version
for file in (string match -v 'build/*' **.qml) for file in (string match -v 'build/*' **.qml)
/usr/lib/qt6/bin/qmlformat $file | diff -u $file - || exit 1 /usr/lib/qt6/bin/qmlformat $file | diff -u $file - || exit 1
end end
@ -34,5 +35,6 @@ jobs:
- name: Check C++ format - name: Check C++ format
shell: fish {0} shell: fish {0}
run: | run: |
clang-format --version
find plugin extras -name '*.cpp' -o -name '*.hpp' \ find plugin extras -name '*.cpp' -o -name '*.hpp' \
| xargs clang-format --dry-run --Werror | xargs clang-format --dry-run --Werror

View file

@ -25,6 +25,8 @@ jobs:
- name: Lint QML - name: Lint QML
shell: fish {0} shell: fish {0}
run: | run: |
/usr/lib/qt6/bin/qmllint --version
# Generate tooling # Generate tooling
touch .qmlls.ini touch .qmlls.ini
QT_QPA_PLATFORM=offscreen QML2_IMPORT_PATH="$PWD/build/qml:$QML2_IMPORT_PATH" timeout 2 qs -p . QT_QPA_PLATFORM=offscreen QML2_IMPORT_PATH="$PWD/build/qml:$QML2_IMPORT_PATH" timeout 2 qs -p .

View file

@ -23,7 +23,13 @@ jobs:
run: | run: |
cat > /tmp/Dockerfile <<EOF cat > /tmp/Dockerfile <<EOF
FROM archlinux:latest FROM archlinux:latest
RUN pacman -Syu --needed --noconfirm sudo base-devel cmake ninja fish git clazy qt6-declarative qt6-shadertools python libpipewire aubio libqalculate && \ # TEMPORARY (remove once the upstream qmlformat regression is fixed): pin to the
# 2026-05-13 Arch archive snapshot to hold qmlformat at qt6-declarative 6.11.0.
# qt6-declarative 6.11.1 silently crashes qmlformat (exit 1, empty output) on QML files
# that declare a property named 'id'. To unpin: delete the mirrorlist line below and
# change 'pacman -Syyuu' back to 'pacman -Syu'.
RUN echo 'Server = https://archive.archlinux.org/repos/2026/05/13/\$repo/os/\$arch' > /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 && \ useradd -m builder && \
echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \ echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \
sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \

View file

@ -10,25 +10,25 @@ import Caelestia.Config
Item { Item {
id: model id: model
property alias visibleModel: _visibleModel property alias visibleModel: visibleModel
property string activeLabel: "" property string activeLabel: ""
property int activeIndex: -1 property int activeIndex: -1
property var _xkbMap: ({}) property var _xkbMap: ({})
property bool _notifiedLimit: false property bool _notifiedLimit: false
function start() { function start() {
_xkbXmlBase.running = true; xkbXmlBase.running = true;
_getKbLayoutOpt.running = true; getKbLayoutOpt.running = true;
} }
function refresh() { function refresh() {
_notifiedLimit = false; _notifiedLimit = false;
_getKbLayoutOpt.running = true; getKbLayoutOpt.running = true;
} }
function switchTo(idx) { function switchTo(idx) {
_switchProc.command = ["hyprctl", "switchxkblayout", "all", String(idx)]; switchProc.command = ["hyprctl", "switchxkblayout", "all", String(idx)];
_switchProc.running = true; switchProc.running = true;
} }
function _buildXmlMap(xml) { function _buildXmlMap(xml) {
@ -50,19 +50,19 @@ Item {
_xkbMap = map; _xkbMap = map;
if (_layoutsModel.count > 0) { if (layoutsModel.count > 0) {
const tmp = []; const tmp = [];
for (let i = 0; i < _layoutsModel.count; i++) { for (let i = 0; i < layoutsModel.count; i++) {
const it = _layoutsModel.get(i); const it = layoutsModel.get(i);
tmp.push({ tmp.push({
layoutIndex: it.layoutIndex, layoutIndex: it.layoutIndex,
token: it.token, token: it.token,
label: _pretty(it.token) label: _pretty(it.token)
}); });
} }
_layoutsModel.clear(); layoutsModel.clear();
tmp.forEach(t => _layoutsModel.append(t)); tmp.forEach(t => layoutsModel.append(t));
_fetchActiveLayouts.running = true; fetchActiveLayouts.running = true;
} }
} }
@ -78,7 +78,7 @@ Item {
function _setLayouts(raw) { function _setLayouts(raw) {
const parts = raw.split(",").map(s => s.trim()).filter(Boolean); const parts = raw.split(",").map(s => s.trim()).filter(Boolean);
_layoutsModel.clear(); layoutsModel.clear();
const seen = new Set(); const seen = new Set();
let idx = 0; let idx = 0;
@ -87,7 +87,7 @@ Item {
if (seen.has(p)) if (seen.has(p))
continue; continue;
seen.add(p); seen.add(p);
_layoutsModel.append({ layoutsModel.append({
layoutIndex: idx, layoutIndex: idx,
token: p, token: p,
label: _pretty(p) label: _pretty(p)
@ -97,19 +97,19 @@ Item {
} }
function _rebuildVisible() { function _rebuildVisible() {
_visibleModel.clear(); visibleModel.clear();
let arr = []; let arr = [];
for (let i = 0; i < _layoutsModel.count; i++) for (let i = 0; i < layoutsModel.count; i++)
arr.push(_layoutsModel.get(i)); arr.push(layoutsModel.get(i));
arr = arr.filter(i => i.layoutIndex !== activeIndex); arr = arr.filter(i => i.layoutIndex !== activeIndex);
arr.forEach(i => _visibleModel.append(i)); arr.forEach(i => visibleModel.append(i));
if (!GlobalConfig.utilities.toasts.kbLimit) if (!GlobalConfig.utilities.toasts.kbLimit)
return; 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"); Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning");
} }
} }
@ -124,26 +124,26 @@ Item {
visible: false visible: false
ListModel { ListModel {
id: _visibleModel id: visibleModel
} }
ListModel { ListModel {
id: _layoutsModel id: layoutsModel
} }
Process { Process {
id: _xkbXmlBase id: xkbXmlBase
command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: model._buildXmlMap(text) onStreamFinished: model._buildXmlMap(text)
} }
onRunningChanged: if (!running && (typeof _xkbXmlBase.exitCode !== "undefined") && _xkbXmlBase.exitCode !== 0) // qmllint disable missing-property onRunningChanged: if (!running && (typeof xkbXmlBase.exitCode !== "undefined") && xkbXmlBase.exitCode !== 0) // qmllint disable missing-property
_xkbXmlEvdev.running = true xkbXmlEvdev.running = true
} }
Process { Process {
id: _xkbXmlEvdev id: xkbXmlEvdev
command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"]
stdout: StdioCollector { stdout: StdioCollector {
@ -152,7 +152,7 @@ Item {
} }
Process { Process {
id: _getKbLayoutOpt id: getKbLayoutOpt
command: ["hyprctl", "-j", "getoption", "input:kb_layout"] command: ["hyprctl", "-j", "getoption", "input:kb_layout"]
stdout: StdioCollector { stdout: StdioCollector {
@ -162,17 +162,17 @@ Item {
const raw = (j?.str || j?.value || "").toString().trim(); const raw = (j?.str || j?.value || "").toString().trim();
if (raw.length) { if (raw.length) {
model._setLayouts(raw); model._setLayouts(raw);
_fetchActiveLayouts.running = true; fetchActiveLayouts.running = true;
return; return;
} }
} catch (e) {} } catch (e) {}
_fetchLayoutsFromDevices.running = true; fetchLayoutsFromDevices.running = true;
} }
} }
} }
Process { Process {
id: _fetchLayoutsFromDevices id: fetchLayoutsFromDevices
command: ["hyprctl", "-j", "devices"] command: ["hyprctl", "-j", "devices"]
stdout: StdioCollector { stdout: StdioCollector {
@ -184,13 +184,13 @@ Item {
if (raw.length) if (raw.length)
model._setLayouts(raw); model._setLayouts(raw);
} catch (e) {} } catch (e) {}
_fetchActiveLayouts.running = true; fetchActiveLayouts.running = true;
} }
} }
} }
Process { Process {
id: _fetchActiveLayouts id: fetchActiveLayouts
command: ["hyprctl", "-j", "devices"] command: ["hyprctl", "-j", "devices"]
stdout: StdioCollector { stdout: StdioCollector {
@ -201,7 +201,7 @@ Item {
const idx = kb?.active_layout_index ?? -1; const idx = kb?.active_layout_index ?? -1;
model.activeIndex = idx >= 0 ? idx : -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) { } catch (e) {
model.activeIndex = -1; model.activeIndex = -1;
model.activeLabel = ""; model.activeLabel = "";
@ -213,9 +213,9 @@ Item {
} }
Process { Process {
id: _switchProc id: switchProc
onRunningChanged: if (!running) onRunningChanged: if (!running)
_fetchActiveLayouts.running = true fetchActiveLayouts.running = true
} }
} }

View file

@ -32,6 +32,18 @@ static float cpuSmoothstep(float edge0, float edge1, float x) {
return t * t * (3.0f - 2.0f * t); 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) BlobShape::BlobShape(QQuickItem* parent)
: QQuickItem(parent) { : QQuickItem(parent) {
setFlag(ItemHasContents); setFlag(ItemHasContents);
@ -310,20 +322,16 @@ void BlobShape::updatePolish() {
if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si)) if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si))
continue; continue;
const auto& rj = m_cachedRects[j]; const auto& rj = m_cachedRects[j];
// Skip when the corner is inside rj: it's buried, not on a visible junction, // Square each corner only near rj's edge; keep full radius far outside AND
// so squaring it would only perturb interior SDF gradients. // 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 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 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 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); const float sdTl = cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh);
if (sdTr >= 0.0f) fTr = std::min(fTr, cornerFillFactor(sdTr, smoothFactor));
fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, sdTr)); fBr = std::min(fBr, cornerFillFactor(sdBr, smoothFactor));
if (sdBr >= 0.0f) fBl = std::min(fBl, cornerFillFactor(sdBl, smoothFactor));
fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, sdBr)); fTl = std::min(fTl, cornerFillFactor(sdTl, smoothFactor));
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));
} }
if (cornerFill && m_cachedHasInverted) { if (cornerFill && m_cachedHasInverted) {

View file

@ -42,9 +42,12 @@ float sdBox(vec2 p, vec2 center, vec2 halfSize) {
float smin(float a, float b, float k) { float smin(float a, float b, float k) {
// Circular smooth min — the blend fillet is a true circular arc of radius 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 // tangent to both surfaces (not a polynomial/squircle curve). Deviates from
// width = k (deviates only where both a < k and b < k), matching the cubic it // min(a, b) only in the corner region where BOTH a < k and b < k (unlike the
// replaces. Always <= min(a, b); max blend depth at a == b is (sqrt(2) - 1) * k. // 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))); return max(k, min(a, b)) - length(max(vec2(k) - vec2(a, b), vec2(0.0)));
} }
@ -164,8 +167,8 @@ void main() {
continue; continue;
if ((excludeMask & (1 << j)) != 0) if ((excludeMask & (1 << j)) != 0)
continue; continue;
// smin only deviates from min within smoothFactor // Circular smin deviates from min only where BOTH dArr are < smoothFactor.
if (abs(dArr[i] - dArr[j]) >= smoothFactor) if (max(dArr[i], dArr[j]) >= smoothFactor)
continue; continue;
mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor)); 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 // Screen-space center (with offset) and pre-computed AABB half-extents
vec2 ctr = rect.xy + sinkProps.yz; vec2 ctr = rect.xy + sinkProps.yz;
// Delay sink to absorb smin blend depth (circular smin max = (sqrt(2)-1)*k) // Sink onset / residual overlap: how far a rect must penetrate the border before
float preOff = smoothFactor * (sqrt(2.0) - 1.0); // 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 // Top border: track rect's BOTTOM edge, only within border thickness
float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, innerTop - outerTop); float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, innerTop - outerTop);
@ -231,7 +239,16 @@ void main() {
dInner -= sinkValue; 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); mergedSdf = smin(mergedSdf, dFrame, smoothFactor);
if (dFrame < minDist) { if (dFrame < minDist) {

View file

@ -20,8 +20,8 @@ Singleton {
readonly property real uploadTotal: _uploadTotal readonly property real uploadTotal: _uploadTotal
// History buffers for sparkline // History buffers for sparkline
readonly property CircularBuffer downloadBuffer: _downloadBuffer readonly property alias downloadBuffer: downloadHistory
readonly property CircularBuffer uploadBuffer: _uploadBuffer readonly property alias uploadBuffer: uploadHistory
readonly property int historyLength: 30 readonly property int historyLength: 30
// Private properties // Private properties
@ -137,13 +137,13 @@ Singleton {
} }
CircularBuffer { CircularBuffer {
id: _downloadBuffer id: downloadHistory
capacity: root.historyLength + 1 capacity: root.historyLength + 1
} }
CircularBuffer { CircularBuffer {
id: _uploadBuffer id: uploadHistory
capacity: root.historyLength + 1 capacity: root.historyLength + 1
} }
@ -200,10 +200,10 @@ Singleton {
root._uploadSpeed = txDelta / timeDelta; root._uploadSpeed = txDelta / timeDelta;
if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed))
_downloadBuffer.push(root._downloadSpeed); downloadHistory.push(root._downloadSpeed);
if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed))
_uploadBuffer.push(root._uploadSpeed); uploadHistory.push(root._uploadSpeed);
} }
// Calculate totals with overflow handling // Calculate totals with overflow handling