feat: replace notif group list with lazy list view

This commit is contained in:
2 * r + 2 * t 2026-04-03 17:59:27 +11:00
parent cf8b68be3b
commit 20983ffa6e
3 changed files with 146 additions and 163 deletions

View file

@ -1,13 +1,12 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Caelestia.Components
import qs.components import qs.components
import qs.services import qs.services
import qs.config import qs.config
Item { LazyListView {
id: root id: root
required property Props props required property Props props
@ -16,19 +15,8 @@ Item {
required property Flickable container required property Flickable container
required property DrawerVisibilities visibilities required property DrawerVisibilities visibilities
readonly property real nonAnimHeight: { readonly property real nonAnimHeight: layoutHeight
let h = -root.spacing;
for (let i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i) as NotifDelegate;
if (item && !item.modelData.closed && !item.previewHidden)
h += item.nonAnimHeight + root.spacing;
}
return h;
}
readonly property int spacing: Math.round(Appearance.spacing.small / 2)
property bool showAllNotifs property bool showAllNotifs
property bool flag
signal requestToggleExpand(expand: bool) signal requestToggleExpand(expand: bool)
@ -42,7 +30,15 @@ Item {
} }
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: nonAnimHeight implicitHeight: contentHeight
spacing: Math.round(Appearance.spacing.small / 2)
removeDuration: Appearance.anim.durations.normal
useCustomViewport: true
viewport: Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y,
width, container.height)
Timer { Timer {
id: clearTimer id: clearTimer
@ -51,74 +47,45 @@ Item {
onTriggered: root.showAllNotifs = false onTriggered: root.showAllNotifs = false
} }
Repeater {
id: repeater
model: ScriptModel { model: ScriptModel {
values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1)
onValuesChanged: root.flagChanged()
} }
delegate: NotifDelegate {} delegate: Component {
} MouseArea {
Behavior on implicitHeight {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
component NotifDelegate: MouseArea {
id: notif id: notif
required property int index required property int index
required property NotifData modelData required property NotifData modelData
readonly property alias nonAnimHeight: notifInner.nonAnimHeight
readonly property bool previewHidden: { readonly property bool previewHidden: {
if (root.expanded) if (root.expanded)
return false; return false;
let extraHidden = 0; let extraHidden = 0;
for (let i = 0; i < index; i++) for (let i = 0; i < index; i++)
if (root.notifs[i].closed) if (root.notifs[i]?.closed)
extraHidden++; extraHidden++;
return index >= Config.notifs.groupPreviewNum + extraHidden; return index >= Config.notifs.groupPreviewNum + extraHidden;
} }
property int startY property int startY
y: { Component.onCompleted: modelData?.lock(this)
root.flag; // Force update Component.onDestruction: modelData?.unlock(this)
let y = 0;
for (let i = 0; i < index; i++) {
const item = repeater.itemAt(i) as NotifDelegate;
if (item && !item.modelData.closed && !item.previewHidden)
y += item.nonAnimHeight + root.spacing;
}
return y;
}
containmentMask: QtObject { LazyListView.preferredHeight: modelData?.closed || previewHidden ? 0 : notifInner.nonAnimHeight
function contains(p: point): bool { LazyListView.visibleHeight: modelData?.closed || previewHidden ? 0 : notifInner.implicitHeight
if (!root.container.contains(notif.mapToItem(root.container, p)))
return false;
return notifInner.contains(p);
}
}
opacity: previewHidden ? 0 : 1
scale: previewHidden ? 0.7 : 1
implicitWidth: root.width
implicitHeight: notifInner.implicitHeight implicitHeight: notifInner.implicitHeight
opacity: LazyListView.removing || modelData?.closed || previewHidden || LazyListView.adding ? 0 : 1
scale: LazyListView.removing || previewHidden ? 0.7 : LazyListView.adding ? 0.7 : 1
hoverEnabled: true hoverEnabled: true
cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
preventStealing: !root.expanded preventStealing: !root.expanded
enabled: !modelData.closed enabled: !(modelData?.closed ?? true)
drag.target: this drag.target: this
drag.axis: Drag.XAxis drag.axis: Drag.XAxis
@ -128,7 +95,7 @@ Item {
if (event.button === Qt.RightButton) if (event.button === Qt.RightButton)
root.requestToggleExpand(!root.expanded); root.requestToggleExpand(!root.expanded);
else if (event.button === Qt.MiddleButton) else if (event.button === Qt.MiddleButton)
modelData.close(); modelData?.close();
} }
onPositionChanged: event => { onPositionChanged: event => {
if (pressed && !root.expanded) { if (pressed && !root.expanded) {
@ -141,32 +108,12 @@ Item {
if (Math.abs(x) < width * Config.notifs.clearThreshold) if (Math.abs(x) < width * Config.notifs.clearThreshold)
x = 0; x = 0;
else else
modelData.close(); modelData?.close();
}
Component.onCompleted: modelData.lock(this)
Component.onDestruction: modelData.unlock(this)
ParallelAnimation {
Component.onCompleted: running = !notif.previewHidden
Anim {
target: notif
property: "opacity"
from: 0
to: 1
}
Anim {
target: notif
property: "scale"
from: 0.7
to: 1
}
} }
ParallelAnimation { ParallelAnimation {
running: notif.modelData.closed running: notif.modelData?.closed ?? false
onFinished: notif.modelData.unlock(notif) onFinished: notif.modelData?.unlock(notif)
Anim { Anim {
target: notif target: notif
@ -190,6 +137,13 @@ Item {
visibilities: root.visibilities visibilities: root.visibilities
} }
Behavior on y {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on opacity { Behavior on opacity {
Anim {} Anim {}
} }
@ -204,12 +158,6 @@ Item {
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
} }
} }
Behavior on y {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
} }
} }
} }

View file

@ -126,6 +126,10 @@ qreal LazyListView::contentHeight() const {
return m_contentHeight; return m_contentHeight;
} }
qreal LazyListView::layoutHeight() const {
return m_layoutHeight;
}
qreal LazyListView::contentY() const { qreal LazyListView::contentY() const {
return m_contentY; return m_contentY;
} }
@ -413,33 +417,55 @@ void LazyListView::updatePolish() {
// --- Layout Engine --- // --- Layout Engine ---
void LazyListView::relayout() { void LazyListView::relayout() {
// Layout positioning uses preferredHeight (final/non-animated) // Layout positioning uses preferredHeight (final/non-animated).
// Only add spacing between items with non-zero height.
qreal y = 0; qreal y = 0;
bool hasLayoutItem = false;
for (auto& record : m_layout) { for (auto& record : m_layout) {
record.targetY = y; record.targetY = y;
y += (record.heightKnown ? record.height : effectiveEstimatedHeight()) + m_spacing; const qreal layoutH = record.heightKnown ? record.height : effectiveEstimatedHeight();
if (layoutH > 0) {
if (hasLayoutItem)
y += m_spacing;
hasLayoutItem = true;
y += layoutH;
}
} }
// Content height tracks actual visible heights so scrolling follows animations if (!qFuzzyCompare(m_layoutHeight, y)) {
m_layoutHeight = y;
emit layoutHeightChanged();
}
// Content height tracks actual visible heights so scrolling follows animations.
// Only add spacing between items with non-zero visible height.
qreal visY = 0; qreal visY = 0;
bool hasVisItem = false;
for (int i = 0; i < static_cast<int>(m_layout.size()); ++i) { for (int i = 0; i < static_cast<int>(m_layout.size()); ++i) {
qreal h; qreal h;
if (m_delegates.contains(i) && m_delegates[i].item) if (m_delegates.contains(i) && m_delegates[i].item)
h = delegateVisibleHeight(m_delegates[i].item); h = delegateVisibleHeight(m_delegates[i].item);
else else
h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight();
visY += h + m_spacing; if (h > 0) {
if (hasVisItem)
visY += m_spacing;
hasVisItem = true;
visY += h;
}
} }
qreal maxBottom = m_layout.isEmpty() ? 0 : visY - m_spacing;
// Account for dying delegates still visually present // Account for dying delegates still visually present
for (const auto& dying : std::as_const(m_dyingDelegates)) { for (const auto& dying : std::as_const(m_dyingDelegates)) {
if (dying.item) if (!dying.item)
maxBottom = std::max(maxBottom, dying.item->y() + delegateVisibleHeight(dying.item)); continue;
const qreal dyingH = delegateVisibleHeight(dying.item);
if (dyingH > 0)
visY = std::max(visY, dying.item->y() + dyingH);
} }
if (!qFuzzyCompare(m_contentHeight, maxBottom)) { if (!qFuzzyCompare(m_contentHeight, visY)) {
m_contentHeight = maxBottom; m_contentHeight = visY;
emit contentHeightChanged(); emit contentHeightChanged();
} }
} }
@ -525,7 +551,9 @@ void LazyListView::syncDelegates() {
if (entry.item) { if (entry.item) {
// Measure height (prefer attached preferredHeight, fall back to implicitHeight) // Measure height (prefer attached preferredHeight, fall back to implicitHeight)
const qreal h = delegateHeight(entry.item); const qreal h = delegateHeight(entry.item);
if (h > 0 && !m_layout[i].heightKnown) { if (!m_layout[i].heightKnown || !qFuzzyCompare(m_layout[i].height, h)) {
if (m_layout[i].heightKnown)
untrackHeight(m_layout[i].height);
m_layout[i].height = h; m_layout[i].height = h;
m_layout[i].heightKnown = true; m_layout[i].heightKnown = true;
trackHeight(h); trackHeight(h);
@ -625,6 +653,9 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
if (wasKnown) if (wasKnown)
untrackHeight(oldH); untrackHeight(oldH);
trackHeight(h); trackHeight(h);
// Relayout immediately so layoutHeight/contentHeight update
// synchronously for parent bindings, then polish for delegate sync.
relayout();
polish(); polish();
} }
return; return;

View file

@ -62,6 +62,7 @@ class LazyListView : public QQuickItem {
// Layout // Layout
Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
Q_PROPERTY(qreal contentHeight READ contentHeight NOTIFY contentHeightChanged) Q_PROPERTY(qreal contentHeight READ contentHeight NOTIFY contentHeightChanged)
Q_PROPERTY(qreal layoutHeight READ layoutHeight NOTIFY layoutHeightChanged)
Q_PROPERTY(qreal contentY READ contentY WRITE setContentY NOTIFY contentYChanged) Q_PROPERTY(qreal contentY READ contentY WRITE setContentY NOTIFY contentYChanged)
// Viewport & Lazy Loading // Viewport & Lazy Loading
@ -110,6 +111,7 @@ public:
void setSpacing(qreal spacing); void setSpacing(qreal spacing);
[[nodiscard]] qreal contentHeight() const; [[nodiscard]] qreal contentHeight() const;
[[nodiscard]] qreal layoutHeight() const;
[[nodiscard]] qreal contentY() const; [[nodiscard]] qreal contentY() const;
void setContentY(qreal contentY); void setContentY(qreal contentY);
@ -170,6 +172,7 @@ signals:
void delegateChanged(); void delegateChanged();
void spacingChanged(); void spacingChanged();
void contentHeightChanged(); void contentHeightChanged();
void layoutHeightChanged();
void contentYChanged(); void contentYChanged();
void viewportChanged(); void viewportChanged();
void useCustomViewportChanged(); void useCustomViewportChanged();
@ -248,6 +251,7 @@ private:
qreal m_spacing = 0; qreal m_spacing = 0;
qreal m_contentHeight = 0; qreal m_contentHeight = 0;
qreal m_layoutHeight = 0;
qreal m_contentY = 0; qreal m_contentY = 0;
QRectF m_viewport; QRectF m_viewport;