reverse-scroll chat: start at bottom, lazy-load on scroll up
- Model shows last 40 blocks initially, positioned at end - Scrolling near top triggers loadEarlier() which prepends 30 blocks - Scroll position maintained after prepend via contentY adjustment - All blocks still parsed (for searching later) but only tail rendered - Reduced initial render cost significantly
This commit is contained in:
parent
f1a045077b
commit
f122a69bce
|
|
@ -81,25 +81,33 @@ void ChatBlockModel::setRawText(const QString &text)
|
|||
emit rawTextChanged();
|
||||
}
|
||||
|
||||
static constexpr int MAX_VISIBLE_BLOCKS = 80;
|
||||
static constexpr int INITIAL_BLOCKS = 40;
|
||||
static constexpr int LOAD_MORE_COUNT = 30;
|
||||
|
||||
void ChatBlockModel::reparse()
|
||||
{
|
||||
QList<ChatBlock> allBlocks = parseBlocks(m_rawText);
|
||||
m_allBlocks = parseBlocks(m_rawText);
|
||||
|
||||
// Only keep the last N blocks to avoid rendering hundreds of delegates
|
||||
QList<ChatBlock> newBlocks;
|
||||
if (allBlocks.size() > MAX_VISIBLE_BLOCKS) {
|
||||
newBlocks = allBlocks.mid(allBlocks.size() - MAX_VISIBLE_BLOCKS);
|
||||
// Determine visible window: last INITIAL_BLOCKS (or expand if already showing more)
|
||||
int desiredStart;
|
||||
if (m_blocks.isEmpty()) {
|
||||
// Initial load: show last N blocks
|
||||
desiredStart = qMax(0, m_allBlocks.size() - INITIAL_BLOCKS);
|
||||
} else {
|
||||
newBlocks = allBlocks;
|
||||
// Streaming update: keep the same start offset, just extend the tail
|
||||
desiredStart = m_visibleStart;
|
||||
// But if allBlocks shrank (session switch), reset
|
||||
if (desiredStart >= m_allBlocks.size())
|
||||
desiredStart = qMax(0, m_allBlocks.size() - INITIAL_BLOCKS);
|
||||
}
|
||||
m_visibleStart = desiredStart;
|
||||
|
||||
QList<ChatBlock> newBlocks = m_allBlocks.mid(m_visibleStart);
|
||||
|
||||
// Incremental update: only change what differs
|
||||
int commonCount = qMin(m_blocks.size(), newBlocks.size());
|
||||
int firstDiff = commonCount; // index of first different block
|
||||
int firstDiff = commonCount;
|
||||
|
||||
// Find first block that changed (usually the last one during streaming)
|
||||
for (int i = 0; i < commonCount; ++i) {
|
||||
if (m_blocks[i].content != newBlocks[i].content ||
|
||||
m_blocks[i].type != newBlocks[i].type ||
|
||||
|
|
@ -122,7 +130,7 @@ void ChatBlockModel::reparse()
|
|||
emit dataChanged(index(i), index(i));
|
||||
}
|
||||
|
||||
// Append new blocks
|
||||
// Append new blocks at end
|
||||
if (newBlocks.size() > m_blocks.size()) {
|
||||
beginInsertRows(QModelIndex(), m_blocks.size(), newBlocks.size() - 1);
|
||||
for (int i = m_blocks.size(); i < newBlocks.size(); ++i) {
|
||||
|
|
@ -132,6 +140,26 @@ void ChatBlockModel::reparse()
|
|||
}
|
||||
}
|
||||
|
||||
bool ChatBlockModel::loadEarlier()
|
||||
{
|
||||
if (m_visibleStart <= 0)
|
||||
return false; // already showing everything
|
||||
|
||||
int loadCount = qMin(LOAD_MORE_COUNT, m_visibleStart);
|
||||
int newStart = m_visibleStart - loadCount;
|
||||
|
||||
QList<ChatBlock> prepend = m_allBlocks.mid(newStart, loadCount);
|
||||
|
||||
beginInsertRows(QModelIndex(), 0, loadCount - 1);
|
||||
for (int i = loadCount - 1; i >= 0; --i) {
|
||||
m_blocks.prepend(prepend[i]);
|
||||
}
|
||||
m_visibleStart = newStart;
|
||||
endInsertRows();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<ChatBlock> ChatBlockModel::parseBlocks(const QString &text) const
|
||||
{
|
||||
QList<ChatBlock> blocks;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ public:
|
|||
|
||||
Q_INVOKABLE QString highlightedHtml(const QString &code, const QString &language) const;
|
||||
Q_INVOKABLE void copyToClipboard(const QString &text) const;
|
||||
Q_INVOKABLE bool loadEarlier(); // returns true if more blocks were loaded
|
||||
|
||||
signals:
|
||||
void rawTextChanged();
|
||||
|
|
@ -71,8 +72,10 @@ private:
|
|||
QVariantList parseSubBlocks(const QString &content) const;
|
||||
|
||||
QString m_rawText;
|
||||
QList<ChatBlock> m_blocks;
|
||||
mutable QHash<QString, QString> m_highlightCache; // key: code+lang, value: html
|
||||
QList<ChatBlock> m_allBlocks; // all parsed blocks
|
||||
QList<ChatBlock> m_blocks; // visible window (tail of m_allBlocks)
|
||||
int m_visibleStart = 0; // index into m_allBlocks where visible window starts
|
||||
mutable QHash<QString, QString> m_highlightCache;
|
||||
};
|
||||
|
||||
#endif // CHAT_BLOCK_MODEL_H
|
||||
|
|
|
|||
16
gui/main.qml
16
gui/main.qml
|
|
@ -371,13 +371,29 @@ ApplicationWindow {
|
|||
spacing: 8
|
||||
cacheBuffer: 200
|
||||
model: chatModel
|
||||
verticalLayoutDirection: ListView.TopToBottom
|
||||
|
||||
property bool _scrolling: false
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(function() { chatList.positionViewAtEnd() })
|
||||
}
|
||||
|
||||
onContentYChanged: {
|
||||
if (_scrolling) return
|
||||
let bottom = contentHeight - height
|
||||
parent.atBottom = (bottom <= 0 || contentY >= bottom - 4)
|
||||
|
||||
// Load earlier messages when scrolled near top
|
||||
if (contentY < 100 && contentHeight > height) {
|
||||
let oldHeight = contentHeight
|
||||
if (chatModel.loadEarlier()) {
|
||||
// Maintain scroll position after prepend
|
||||
Qt.callLater(function() {
|
||||
chatList.contentY += (chatList.contentHeight - oldHeight)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
onContentHeightChanged: {
|
||||
if (parent.atBottom && !_scrolling) {
|
||||
|
|
|
|||
Reference in New Issue