From 22d94428f6800fa1e56f40f895d6512329149264 Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Mon, 17 Aug 2026 17:14:57 +0100 Subject: [PATCH] Scroll the machine list independently of the panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With a dozen-plus peers the whole panel scrolled, pushing the header, THIS MACHINE, CONNECTIONS and EXIT NODES off the top to reach the machines. The list now has its own Flickable, capped at Style.space(260) — about five rows — so those sections stay in view while the machines move under them. The cap only applies once it is needed: implicitHeight is min(content, max), so a tailnet with three machines still renders as a short panel. The outer scroller stays for when the panel as a whole outgrows the screen. scrollItemIntoView() took a single hardcoded scroller, which would have left j/k in the peers section scrolling the panel rather than the list, and the highlighted row hidden inside a list that never moved. It now takes the target scroller, defaulting to the panel; peer navigation scrolls the list into view first, then the row within it. Co-Authored-By: Claude Opus 5 --- Panel.qml | 72 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/Panel.qml b/Panel.qml index d6ed7d8..332b7e4 100644 --- a/Panel.qml +++ b/Panel.qml @@ -267,24 +267,34 @@ Panel { if (region) chooseExitNode(region) } - function scrollItemIntoView(item) { - if (!panelFlick || !item) return + // `flick` defaults to the whole-panel scroller. The machine list has its own + // Flickable, so rows in that section pass it explicitly — scrolling the outer + // one to reveal a peer would move the panel rather than the list, and the row + // would stay hidden inside a list that never scrolled. + function scrollItemIntoView(item, flick) { + var view = flick || panelFlick + if (!view || !item) return Qt.callLater(function() { if (!item) return var margin = Style.space(6) - var point = item.mapToItem(panelFlick.contentItem, 0, 0) + var point = item.mapToItem(view.contentItem, 0, 0) var top = point.y var bottom = top + item.height - var viewTop = panelFlick.contentY - var viewBottom = viewTop + panelFlick.height - var maxY = Math.max(0, panelFlick.contentHeight - panelFlick.height) - if (top < viewTop + margin) panelFlick.contentY = Math.max(0, top - margin) - else if (bottom > viewBottom - margin) panelFlick.contentY = Math.min(maxY, bottom + margin - panelFlick.height) + var viewTop = view.contentY + var viewBottom = viewTop + view.height + var maxY = Math.max(0, view.contentHeight - view.height) + if (top < viewTop + margin) view.contentY = Math.max(0, top - margin) + else if (bottom > viewBottom - margin) view.contentY = Math.min(maxY, bottom + margin - view.height) }) } function scrollCursorIntoView() { - if (focusSection === "peers" && peerColumn && peerIndex >= 0 && peerIndex < peerColumn.children.length) scrollItemIntoView(peerColumn.children[peerIndex]) + if (focusSection === "peers" && peerColumn && peerIndex >= 0 && peerIndex < peerColumn.children.length) { + // Bring the machine list itself into view first, then the row within it — + // otherwise the row scrolls inside a list that is still off-screen. + scrollItemIntoView(peerFlick) + scrollItemIntoView(peerColumn.children[peerIndex], peerFlick) + } else if (focusSection === "exitNodes" && exitNodeColumn && exitNodeIndex >= 0 && exitNodeIndex < exitNodeColumn.children.length) scrollItemIntoView(exitNodeColumn.children[exitNodeIndex]) } @@ -825,20 +835,42 @@ Panel { horizontalAlignment: Text.AlignHCenter } - Column { - id: peerColumn + // The machine list scrolls on its own rather than pushing the + // sections above it off the top of the panel. Its height is the + // content height until that passes maxPeerListHeight, after which + // it stops growing and scrolls internally — so a tailnet with three + // machines still renders as a short panel, and one with thirty + // keeps the header, THIS MACHINE, CONNECTIONS and EXIT NODES in + // view while the machines move under them. + Flickable { + id: peerFlick visible: root.showPeers width: parent.width - spacing: Style.space(6) + readonly property real maxPeerListHeight: Style.space(260) + implicitHeight: Math.min(peerColumn.implicitHeight, maxPeerListHeight) + height: implicitHeight + contentWidth: width + contentHeight: peerColumn.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + interactive: contentHeight > height + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } - Repeater { - model: tailscale.peers - PeerRow { - required property var modelData - required property int index - width: peerColumn.width - peer: modelData - rowIndex: index + Column { + id: peerColumn + width: peerFlick.width + spacing: Style.space(6) + + Repeater { + model: tailscale.peers + PeerRow { + required property var modelData + required property int index + width: peerColumn.width + peer: modelData + rowIndex: index + } } } }