Polish main workspace UI density and packet detail usability#4
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refines the main workspace UI density and usability across both the Qt (QML) UI and the experimental Tauri UI, with a focus on making the Flows workspace more compact and improving Packet Details Summary copy/select behavior.
Changes:
- Tightened spacing and row/tab heights across Flows workspace panels (Qt + Tauri).
- Replaced separate Address/Port columns with compact Endpoint A/B rendering (Qt + Tauri).
- Consolidated the lower Packets/Stream controls into a single toolbar-style row and made Qt Summary text selectable.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ui/qml/Main.qml | Reduced top-level tab spacing and tab heights for denser layout. |
| src/ui/qml/components/StreamView.qml | Added showToolbar and tightened stream view spacing/typography. |
| src/ui/qml/components/PacketList.qml | Added showToolbar, tightened list density, and adjusted row heights. |
| src/ui/qml/components/PacketDetailsPane.qml | Introduced selectable summary text via SelectableText and reduced vertical spacing. |
| src/ui/qml/components/FlowWorkspacePane.qml | Added combined lower toolbar row (tabs + status + Load more) and hid inner toolbars. |
| src/ui/qml/components/FlowTable.qml | Implemented compact Endpoint A/B columns and reduced flow table density. |
| src/ui/app/main.cpp | Removed BOM/normalized include line start. |
| experimental/tauri-ui-spike/web/styles.css | Adjusted spacing, tab sizing, added endpoint cell layout, and restyled “Open Capture…” button. |
| experimental/tauri-ui-spike/web/main.js | Updated row virtualization height and implemented endpoint formatting/rendering helpers. |
| experimental/tauri-ui-spike/web/index.html | Converted headings to more compact layout and added combined flow-view toolbar row. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| readOnly: true | ||
| selectByMouse: true | ||
| textFormat: TextEdit.PlainText | ||
| wrapMode: textWrapMode |
| Label { | ||
| Layout.fillWidth: true | ||
| text: root.lowerToolbarStatusText() | ||
| color: "#6b7280" | ||
| elide: Text.ElideRight | ||
| verticalAlignment: Text.AlignVCenter | ||
| font.pixelSize: 12 | ||
| } |
|
Addressed.
|
| Label { | ||
| id: endpointALabel | ||
| anchors.fill: parent | ||
| text: endpointAText | ||
| font.family: "Consolas" | ||
| verticalAlignment: Text.AlignVCenter | ||
| } |
| Label { | ||
| id: endpointBLabel | ||
| anchors.fill: parent | ||
| text: endpointBText | ||
| font.family: "Consolas" | ||
| verticalAlignment: Text.AlignVCenter | ||
| } |
| ListView { | ||
| id: flowListView | ||
| readonly property int rightGutter: flowScrollBar.visible ? flowScrollBar.width + 10 : 0 | ||
|
|
||
| anchors.fill: parent |
| function endpointHasPort(port) { | ||
| const numericPort = Number(port) | ||
| return Number.isFinite(numericPort) && numericPort > 0 | ||
| } | ||
|
|
||
| function endpointDisplayAddress(address, port) { | ||
| const trimmedAddress = address ? String(address).trim() : "" | ||
| if (trimmedAddress.length === 0) { | ||
| return "" | ||
| } | ||
|
|
||
| if (root.endpointHasPort(port) && trimmedAddress.indexOf(":") >= 0) { | ||
| return "[" + trimmedAddress + "]" | ||
| } | ||
|
|
||
| return trimmedAddress | ||
| } |
|
Addressed.
|
| component SelectableText: TextEdit { | ||
| property color textColor: "#0f172a" | ||
| property bool monospace: false | ||
| property bool bold: false | ||
| property int textWrapMode: TextEdit.NoWrap | ||
|
|
||
| readOnly: true | ||
| activeFocusOnTab: false | ||
| selectByMouse: true | ||
| textFormat: TextEdit.PlainText | ||
| wrapMode: textWrapMode | ||
| color: textColor | ||
| font.family: monospace ? "Consolas" : "" | ||
| font.pixelSize: 12 | ||
| font.bold: bold | ||
| cursorVisible: activeFocus | ||
| } |
| ToolTip.visible: endpointAHoverArea.containsMouse && endpointAText.length > 0 | ||
| ToolTip.text: endpointAText |
| ToolTip.visible: endpointBHoverArea.containsMouse && endpointBText.length > 0 | ||
| ToolTip.text: endpointBText |
|
Addressed.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
experimental/tauri-ui-spike/web/index.html:184
- Similarly, the Selected-flow packets/stream panel no longer has a semantic heading. Adding an accessible name to the section preserves navigation landmarks for assistive tech without changing the compact visual design.
<section class="panel packets-panel">
<div class="flow-view-toolbar">
<div class="subtabbar" aria-label="Selected flow views">
<button type="button" class="subtab-button active" data-flow-view-tab="packets">Packets</button>
<button type="button" class="subtab-button" data-flow-view-tab="stream">Stream</button>
</div>
<div class="panel-heading-copy flow-view-status-wrap">
<p id="packetMeta" class="panel-subtitle panel-subtitle-inline flow-view-status">Select a flow to load packets.</p>
</div>
<div class="pager-controls">
<button id="packetLoadMoreButton" type="button" class="secondary-button">Load More</button>
<button id="streamLoadMoreButton" type="button" class="secondary-button">Load More</button>
</div>
</div>
| } | ||
| ListView { | ||
| id: flowListView | ||
| readonly property int rightGutter: flowScrollBar.visible ? flowScrollBar.width + 10 : 0 |
| <section class="panel flows-panel"> | ||
| <div class="panel-heading"> | ||
| <div> | ||
| <h2>Flows</h2> | ||
| <p id="flowMeta" class="panel-subtitle">No capture loaded.</p> | ||
| <div class="panel-heading panel-heading-compact"> | ||
| <div class="panel-heading-copy"> | ||
| <p id="flowMeta" class="panel-subtitle panel-subtitle-inline">No capture loaded.</p> | ||
| </div> | ||
| </div> |
|
Addressed.
|
| Item { | ||
| id: flowTableViewport | ||
| Layout.fillWidth: true | ||
| Layout.fillHeight: true | ||
| color: "#f8fafc" | ||
| border.color: "#e2e8f0" | ||
| radius: 6 | ||
|
|
||
| ListView { | ||
| id: flowListView | ||
| readonly property int rightGutter: flowScrollBar.visible ? flowScrollBar.width + 10 : 0 | ||
|
|
||
|
|
||
| Flickable { | ||
| id: flowTableScroller | ||
| anchors.fill: parent | ||
| anchors.margins: 1 | ||
| clip: true | ||
| model: root.flowModel | ||
| currentIndex: -1 | ||
| onCountChanged: root.syncSelectedFlowRow() | ||
| onModelChanged: root.syncSelectedFlowRow() | ||
|
|
||
| ScrollBar.vertical: ScrollBar { | ||
| id: flowScrollBar | ||
| policy: flowListView.contentHeight > flowListView.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff | ||
| contentWidth: root.flowTableContentWidth | ||
| contentHeight: 1 | ||
| flickableDirection: Flickable.HorizontalFlick | ||
| boundsBehavior: Flickable.StopAtBounds | ||
|
|
||
| ScrollBar.horizontal: ScrollBar { | ||
| id: flowTableHorizontalScrollBar | ||
| policy: flowTableScroller.contentWidth > flowTableScroller.width ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff | ||
| } | ||
| } |
| SelectableText { | ||
| Layout.fillWidth: true | ||
| text: summaryLayerCard.titleText | ||
| font.pixelSize: 13 | ||
| font.bold: false | ||
| color: "#0f172a" | ||
| textColor: "#0f172a" | ||
| clip: true | ||
| } |
| loadedPacketRowCount: root.loadedPacketRowCount | ||
| totalPacketRowCount: root.totalPacketRowCount | ||
| canLoadMorePackets: root.canLoadMorePackets | ||
| showToolbar: false |
|
Addressed.
|
| ToolTip.visible: endpointAHoverArea.containsMouse | ||
| && endpointAText.length > 0 | ||
| && endpointALabel.implicitWidth > endpointALabel.width + 1 | ||
| ToolTip.text: endpointAText |
| ToolTip.visible: endpointBHoverArea.containsMouse | ||
| && endpointBText.length > 0 | ||
| && endpointBLabel.implicitWidth > endpointBLabel.width + 1 | ||
| ToolTip.text: endpointBText |
| MouseArea { | ||
| id: addressBHoverArea | ||
| anchors.fill: parent | ||
| acceptedButtons: Qt.NoButton | ||
| anchors.top: parent.top | ||
| anchors.bottom: parent.bottom | ||
| anchors.left: parent.left | ||
| anchors.right: parent.right | ||
| anchors.leftMargin: 52 | ||
| hoverEnabled: true |
| component SelectableText: TextEdit { | ||
| property color textColor: "#0f172a" | ||
| property bool monospace: false | ||
| property bool bold: false | ||
| property int textWrapMode: TextEdit.NoWrap | ||
|
|
||
| readOnly: true | ||
| activeFocusOnTab: false | ||
| selectByMouse: true | ||
| textFormat: TextEdit.PlainText | ||
| wrapMode: textWrapMode | ||
| color: textColor | ||
| font.family: monospace ? "Consolas" : "" | ||
| font.pixelSize: 12 | ||
| font.bold: bold | ||
| cursorVisible: false | ||
| } |
|
Addressed.
|
| property bool packetsPartiallyLoaded: false | ||
| property var loadedPacketRowCount: 0 | ||
| property var totalPacketRowCount: 0 | ||
| property bool canLoadMorePackets: false | ||
| property bool showToolbar: true |
There was a problem hiding this comment.
Acknowledged.
titleText is currently harmless dead API after the compact lower-toolbar cleanup. Since this PR has already gone through several QML layout fixes and the component is stable now, I’m leaving this as-is for this PR to avoid additional churn before merge. It can be removed in a small follow-up cleanup if needed.
Summary
This PR improves the main workspace UI density and usability in both the Qt UI and the experimental Tauri UI.
The main focus is the Flows workspace, where the previous layout used too much vertical and horizontal space after the recent protocol-detail expansion. The goal is not to copy Wireshark’s dense utility-first UI exactly, but to move PcapFlowLab toward a more compact, analyst-friendly workspace while keeping the interface modern and readable.
Main changes
Main Flows workspace density
Open Capture...action.Flow endpoint columns
Replaced separate Qt columns:
with compact endpoint columns:
Updated Qt and Tauri endpoint formatting to show address and port as a compact cluster.
Added clearer separation between address and port while keeping them visually grouped.
Added IPv6 bracket formatting when a port is present:
Avoids misleading
: 0output when a port is missing or invalid.Lower Packets / Stream panel
Packets/Streamtabs, packet status text, andLoad Morebutton into one compact toolbar-style row.Load Morebehavior.Tabs and spacing polish
Flows / Analysis / Statisticstabs.Summary,Raw,Payload, andProtocol.Qt Packet Details copyability
Tauri Open Capture button polish
Open Capture...button to be visually closer to the Qt version.What was intentionally left unchanged
This PR does not change:
Stream details still need a separate design pass later.
Validation
Manually checked the updated UI on representative captures in both Qt and Tauri:
The updated layout fits more useful information on screen while preserving readability and existing behavior.
Notes
This PR is primarily visual/UI polish. The Tauri UI still has some advantages in Summary rendering, while the Qt UI now has improved copy/select behavior for Summary text. Further Packet Details and Stream layout improvements should be handled in follow-up UI branches.