feat: workspace panel visual overhaul, perf optimizations, remove kernel base addresses

Workspace panel:
- Custom WorkspaceDelegate: struct names bright, metadata dimmed, child types in teal
- Search box: monospace font, search icon, bordered with focus highlight
- Selection: accent bar, all fonts synced to 10pt monospace
- Remove rebuildWorkspaceModel from visibilityChanged (fixes double-click refresh)
- Incremental sync (syncProjectExplorer) preserves tree expansion state

Performance:
- childrenOf() O(1) via cached parent→children hash map
- Debounced workspace rebuilds (50ms coalesce)
- Pre-reserve node vector in NodeTree::fromJson
- Benchmark suite (bench_project)

Data:
- Remove kernel baseAddress from Vergilius/WinSDK examples (default to 0x400000)
This commit is contained in:
IChooseYou
2026-03-07 06:47:16 -07:00
committed by IChooseYou
parent 3ab6affa5e
commit 188c27c6e2
9 changed files with 657 additions and 141 deletions

View File

@@ -333,6 +333,7 @@ struct NodeTree {
int pointerSize = 8; // 4 for 32-bit targets, 8 for 64-bit
uint64_t m_nextId = 1;
mutable QHash<uint64_t, int> m_idCache;
mutable QHash<uint64_t, QVector<int>> m_childCache;
int addNode(const Node& n) {
Node copy = n;
@@ -342,13 +343,15 @@ struct NodeTree {
nodes.append(copy);
if (!m_idCache.isEmpty())
m_idCache[copy.id] = idx;
if (!m_childCache.isEmpty())
m_childCache[copy.parentId].append(idx);
return idx;
}
// Reserve a unique ID atomically (for use before pushing undo commands)
uint64_t reserveId() { return m_nextId++; }
void invalidateIdCache() const { m_idCache.clear(); }
void invalidateIdCache() const { m_idCache.clear(); m_childCache.clear(); }
int indexOfId(uint64_t id) const {
if (m_idCache.isEmpty() && !nodes.isEmpty()) {
@@ -359,11 +362,11 @@ struct NodeTree {
}
QVector<int> childrenOf(uint64_t parentId) const {
QVector<int> result;
for (int i = 0; i < nodes.size(); i++) {
if (nodes[i].parentId == parentId) result.append(i);
if (m_childCache.isEmpty() && !nodes.isEmpty()) {
for (int i = 0; i < nodes.size(); i++)
m_childCache[nodes[i].parentId].append(i);
}
return result;
return m_childCache.value(parentId);
}
// Collect node + all descendants (iterative, cycle-safe)
@@ -483,6 +486,7 @@ struct NodeTree {
t.pointerSize = o["pointerSize"].toInt(8);
t.m_nextId = o["nextId"].toString("1").toULongLong();
QJsonArray arr = o["nodes"].toArray();
t.nodes.reserve(arr.size());
for (const auto& v : arr) {
Node n = Node::fromJson(v.toObject());
t.nodes.append(n);

View File

@@ -1,5 +1,4 @@
{
"baseAddress": "FFFFF80000000000",
"nextId": "20010",
"nodes": [
{

View File

@@ -1,5 +1,4 @@
{
"baseAddress": "fffff80000000000",
"nextId": "18212",
"nodes": [
{

View File

@@ -1582,7 +1582,6 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
if (visible) {
m_activeDocDock = dock;
updateWindowTitle();
rebuildWorkspaceModel();
// Sync view toggle buttons to this tab's active pane
auto it = m_tabs.find(dock);
if (it != m_tabs.end()) {
@@ -2388,19 +2387,30 @@ void MainWindow::applyTheme(const Theme& theme) {
if (auto* w = findChild<QWidget*>("resizeGrip"))
static_cast<ResizeGrip*>(w)->setGripColor(theme.textFaint);
// Workspace tree: colors from theme (selection + text)
// Workspace tree: delegate colors, palette, stylesheet
if (m_workspaceDelegate)
m_workspaceDelegate->setThemeColors(theme);
if (m_workspaceTree) {
QPalette tp = m_workspaceTree->palette();
tp.setColor(QPalette::Text, theme.textDim);
tp.setColor(QPalette::Highlight, theme.hover);
tp.setColor(QPalette::Highlight, theme.selected);
tp.setColor(QPalette::HighlightedText, theme.text);
m_workspaceTree->setPalette(tp);
m_workspaceTree->setStyleSheet(QStringLiteral(
"QTreeView { background: %1; border: none; }")
.arg(theme.background.name()));
m_workspaceTree->viewport()->update();
}
if (m_workspaceSearch) {
m_workspaceSearch->setStyleSheet(QStringLiteral(
"QLineEdit { background: %1; color: %2; border: none;"
" border-bottom: 1px solid %3; padding: 4px 6px; }")
.arg(theme.background.name(), theme.textDim.name(), theme.border.name()));
"QLineEdit { background: %1; color: %2; border: 1px solid %3;"
" padding: 4px 8px; }"
"QLineEdit:focus { border-color: %4; }"
"QLineEdit QToolButton { padding: 0px 4px; }"
"QLineEdit QToolButton:hover { background: %5; }")
.arg(theme.background.name(), theme.textDim.name(),
theme.border.name(), theme.borderFocused.name(),
theme.hover.name()));
}
// Dock titlebar: restyle via palette + close button
@@ -2590,15 +2600,17 @@ void MainWindow::setEditorFont(const QString& fontName) {
}
}
}
// Sync workspace tree font (match tab bar size)
if (m_workspaceTree) {
// Sync workspace tree, title, and search font (10pt monospace)
{
QFont wf(fontName, 10);
wf.setFixedPitch(true);
m_workspaceTree->setFont(wf);
if (m_workspaceTree)
m_workspaceTree->setFont(wf);
if (m_dockTitleLabel)
m_dockTitleLabel->setFont(wf);
if (m_workspaceSearch)
m_workspaceSearch->setFont(wf);
}
// Sync dock titlebar font
if (m_dockTitleLabel)
m_dockTitleLabel->setFont(f);
// Sync scanner panel font
if (m_scannerPanel)
m_scannerPanel->setEditorFont(f);
@@ -3314,6 +3326,10 @@ void MainWindow::createWorkspaceDock() {
QPalette lp = m_dockTitleLabel->palette();
lp.setColor(QPalette::WindowText, t.textDim);
m_dockTitleLabel->setPalette(lp);
QSettings s("Reclass", "Reclass");
QFont f(s.value("font", "JetBrains Mono").toString(), 10);
f.setFixedPitch(true);
m_dockTitleLabel->setFont(f);
}
layout->addWidget(m_dockTitleLabel);
@@ -3342,12 +3358,35 @@ void MainWindow::createWorkspaceDock() {
m_workspaceSearch = new QLineEdit(dockContainer);
m_workspaceSearch->setPlaceholderText(QStringLiteral("Search..."));
m_workspaceSearch->setClearButtonEnabled(true);
{
QSettings s("Reclass", "Reclass");
QFont f(s.value("font", "JetBrains Mono").toString(), 10);
f.setFixedPitch(true);
m_workspaceSearch->setFont(f);
}
{
auto* searchAction = m_workspaceSearch->addAction(
QIcon(QStringLiteral(":/vsicons/search.svg")),
QLineEdit::LeadingPosition);
// Find the QToolButton created for the action and shrink its icon
for (auto* btn : m_workspaceSearch->findChildren<QToolButton*>()) {
if (btn->defaultAction() == searchAction) {
btn->setIconSize(QSize(14, 14));
break;
}
}
}
{
const auto& t = ThemeManager::instance().current();
m_workspaceSearch->setStyleSheet(QStringLiteral(
"QLineEdit { background: %1; color: %2; border: none;"
" border-bottom: 1px solid %3; padding: 4px 6px; }")
.arg(t.background.name(), t.textDim.name(), t.border.name()));
"QLineEdit { background: %1; color: %2; border: 1px solid %3;"
" padding: 4px 8px; }"
"QLineEdit:focus { border-color: %4; }"
"QLineEdit QToolButton { padding: 0px 4px; }"
"QLineEdit QToolButton:hover { background: %5; }")
.arg(t.background.name(), t.textDim.name(),
t.border.name(), t.borderFocused.name(),
t.hover.name()));
}
dockLayout->addWidget(m_workspaceSearch);
@@ -3380,14 +3419,22 @@ void MainWindow::createWorkspaceDock() {
m_workspaceTree->collapseAll();
});
// Override palette: selection + hover use theme colors (not default blue)
// Custom delegate for rich text rendering (name bright, metadata dim)
{
const auto& t = ThemeManager::instance().current();
m_workspaceDelegate = new rcx::WorkspaceDelegate(m_workspaceTree);
m_workspaceDelegate->setThemeColors(t);
m_workspaceTree->setItemDelegate(m_workspaceDelegate);
QPalette tp = m_workspaceTree->palette();
tp.setColor(QPalette::Text, t.textDim);
tp.setColor(QPalette::Highlight, t.hover);
tp.setColor(QPalette::Highlight, t.selected);
tp.setColor(QPalette::HighlightedText, t.text);
m_workspaceTree->setPalette(tp);
m_workspaceTree->setStyleSheet(QStringLiteral(
"QTreeView { background: %1; border: none; }")
.arg(t.background.name()));
}
dockLayout->addWidget(m_workspaceTree);
@@ -3714,16 +3761,28 @@ void MainWindow::rebuildAllDocs() {
}
void MainWindow::rebuildWorkspaceModel() {
// Debounce: coalesce rapid calls into a single rebuild
if (!m_workspaceRebuildTimer) {
m_workspaceRebuildTimer = new QTimer(this);
m_workspaceRebuildTimer->setSingleShot(true);
m_workspaceRebuildTimer->setInterval(50);
connect(m_workspaceRebuildTimer, &QTimer::timeout,
this, &MainWindow::rebuildWorkspaceModelNow);
}
m_workspaceRebuildTimer->start();
}
void MainWindow::rebuildWorkspaceModelNow() {
QVector<rcx::TabInfo> tabs;
QSet<RcxDocument*> seenDocs;
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
TabState& tab = it.value();
if (seenDocs.contains(tab.doc)) continue; // skip duplicate doc views
if (seenDocs.contains(tab.doc)) continue;
seenDocs.insert(tab.doc);
QString name = rootName(tab.doc->tree, tab.ctrl->viewRootId());
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
}
rcx::buildProjectExplorer(m_workspaceModel, tabs);
rcx::syncProjectExplorer(m_workspaceModel, tabs);
}
void MainWindow::addRecentFile(const QString& path) {

View File

@@ -24,6 +24,7 @@ namespace rcx {
class McpBridge;
class ShimmerLabel;
class DockGripWidget;
class WorkspaceDelegate;
class MainWindow : public QMainWindow {
Q_OBJECT
@@ -155,11 +156,14 @@ private:
QStandardItemModel* m_workspaceModel = nullptr;
QSortFilterProxyModel* m_workspaceProxy = nullptr;
QLineEdit* m_workspaceSearch = nullptr;
WorkspaceDelegate* m_workspaceDelegate = nullptr;
QLabel* m_dockTitleLabel = nullptr;
QToolButton* m_dockCloseBtn = nullptr;
DockGripWidget* m_dockGrip = nullptr;
void createWorkspaceDock();
void rebuildWorkspaceModel();
void rebuildWorkspaceModel(); // debounced — safe to call frequently
void rebuildWorkspaceModelNow(); // immediate rebuild
QTimer* m_workspaceRebuildTimer = nullptr;
void updateBorderColor(const QColor& color);
// Scanner dock

View File

@@ -1,8 +1,12 @@
#pragma once
#include "core.h"
#include "themes/theme.h"
#include <QIcon>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QStyledItemDelegate>
#include <QPainter>
#include <QApplication>
#include <algorithm>
namespace rcx {
@@ -13,15 +17,85 @@ struct TabInfo {
void* subPtr; // QDockWidget* as void*
};
// Sentinel value stored in UserRole+1 to mark the Project group node.
static constexpr uint64_t kGroupSentinel = ~uint64_t(0);
// Helper: is a Hex padding node
inline bool isHexPad(NodeKind k) {
return k == NodeKind::Hex8 || k == NodeKind::Hex16
|| k == NodeKind::Hex32 || k == NodeKind::Hex64;
}
// Build child rows for a struct item.
inline void buildStructChildren(QStandardItem* item,
const NodeTree* tree, uint64_t structId,
void* subPtr) {
item->removeRows(0, item->rowCount());
QVector<int> members = tree->childrenOf(structId);
std::sort(members.begin(), members.end(), [&](int a, int b) {
return tree->nodes[a].offset < tree->nodes[b].offset;
});
auto memberTypeName = [](const Node& m) -> QString {
if (m.kind == NodeKind::Struct) {
return m.structTypeName.isEmpty() ? m.resolvedClassKeyword()
: m.structTypeName;
}
return QString::fromLatin1(kindToString(m.kind));
};
for (int mi : members) {
const Node& m = tree->nodes[mi];
if (isHexPad(m.kind)) continue;
QString childDisplay = QStringLiteral("%1 %2")
.arg(memberTypeName(m), m.name);
auto* childItem = new QStandardItem(childDisplay);
childItem->setData(QVariant::fromValue(subPtr), Qt::UserRole);
childItem->setData(QVariant::fromValue(m.id), Qt::UserRole + 1);
item->appendRow(childItem);
}
}
// Helper to build display string for a type entry.
inline QString typeDisplayString(const Node* node, const NodeTree* tree) {
auto nameOf = [](const Node* n) {
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
};
if (node->resolvedClassKeyword() == QStringLiteral("enum")) {
return QStringLiteral("%1 (%2) \u2014 %3")
.arg(nameOf(node), node->resolvedClassKeyword(),
QString::number(node->enumMembers.size()));
}
QVector<int> members = tree->childrenOf(node->id);
int vc = 0;
for (int mi : members)
if (!isHexPad(tree->nodes[mi].kind)) ++vc;
return QStringLiteral("%1 (%2) \u2014 %3")
.arg(nameOf(node), node->resolvedClassKeyword(),
QString::number(vc));
}
// Build a new item for a type entry.
inline QStandardItem* makeTypeItem(const Node* node, const NodeTree* tree,
void* subPtr) {
bool isEnum = node->resolvedClassKeyword() == QStringLiteral("enum");
auto* item = new QStandardItem(
QIcon(isEnum ? ":/vsicons/symbol-enum.svg"
: ":/vsicons/symbol-structure.svg"),
typeDisplayString(node, tree));
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
item->setData(QVariant::fromValue(node->id), Qt::UserRole + 1);
if (!isEnum)
buildStructChildren(item, tree, node->id, subPtr);
return item;
}
// Full rebuild — used by benchmarks and first build.
inline void buildProjectExplorer(QStandardItemModel* model,
const QVector<TabInfo>& tabs) {
model->clear();
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
// Collect all top-level structs/enums across all tabs
struct Entry { const Node* node; void* subPtr; const NodeTree* tree; };
QVector<Entry> types, enums;
for (const auto& tab : tabs) {
@@ -36,76 +110,185 @@ inline void buildProjectExplorer(QStandardItemModel* model,
}
}
auto nameOf = [](const Node* n) {
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
};
// Helper: is a Hex padding node
auto isHexPad = [](NodeKind k) {
return k == NodeKind::Hex8 || k == NodeKind::Hex16
|| k == NodeKind::Hex32 || k == NodeKind::Hex64;
};
// Helper: type display string for a member node
auto memberTypeName = [](const Node& m) -> QString {
if (m.kind == NodeKind::Struct) {
QString stn = m.structTypeName.isEmpty() ? m.resolvedClassKeyword()
: m.structTypeName;
return stn;
}
return QString::fromLatin1(kindToString(m.kind));
};
// TODO: re-enable sorting once startup perf is acceptable
// auto countVisible = [&](const Entry& e) { ... };
// std::sort(types.begin(), types.end(), cmpChildren);
// std::sort(enums.begin(), enums.end(), cmpName);
for (const auto& e : types) {
QVector<int> members = e.tree->childrenOf(e.node->id);
// Count non-hex members for display
int visibleCount = 0;
for (int mi : members)
if (!isHexPad(e.tree->nodes[mi].kind)) ++visibleCount;
QString display = QStringLiteral("%1 (%2) \u2014 %3")
.arg(nameOf(e.node), e.node->resolvedClassKeyword(),
QString::number(visibleCount));
auto* item = new QStandardItem(
QIcon(":/vsicons/symbol-structure.svg"), display);
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1);
// Add child rows sorted by offset (skip Hex padding)
std::sort(members.begin(), members.end(), [&](int a, int b) {
return e.tree->nodes[a].offset < e.tree->nodes[b].offset;
});
for (int mi : members) {
const Node& m = e.tree->nodes[mi];
if (isHexPad(m.kind)) continue;
QString childDisplay = QStringLiteral("%1 %2")
.arg(memberTypeName(m), m.name);
auto* childItem = new QStandardItem(childDisplay);
childItem->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
childItem->setData(QVariant::fromValue(m.id), Qt::UserRole + 1);
item->appendRow(childItem);
}
model->appendRow(item);
}
for (const auto& e : enums) {
int count = e.node->enumMembers.size();
QString display = QStringLiteral("%1 (%2) \u2014 %3")
.arg(nameOf(e.node), e.node->resolvedClassKeyword(),
QString::number(count));
auto* item = new QStandardItem(
QIcon(":/vsicons/symbol-enum.svg"), display);
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1);
model->appendRow(item);
}
for (const auto& e : types)
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
for (const auto& e : enums)
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
}
// Incremental sync — preserves tree expansion/scroll state.
inline void syncProjectExplorer(QStandardItemModel* model,
const QVector<TabInfo>& tabs) {
// First call — full build
if (model->rowCount() == 0 && !tabs.isEmpty()) {
buildProjectExplorer(model, tabs);
return;
}
// Collect desired entries
struct Entry { uint64_t id; const Node* node; void* subPtr; const NodeTree* tree; bool isEnum; };
QVector<Entry> desired;
for (const auto& tab : tabs) {
QVector<int> topLevel = tab.tree->childrenOf(0);
for (int idx : topLevel) {
const Node& n = tab.tree->nodes[idx];
if (n.kind != NodeKind::Struct) continue;
bool ie = n.resolvedClassKeyword() == QStringLiteral("enum");
desired.append({n.id, &n, tab.subPtr, tab.tree, ie});
}
}
QHash<uint64_t, int> desiredMap;
desiredMap.reserve(desired.size());
for (int i = 0; i < desired.size(); ++i)
desiredMap[desired[i].id] = i;
// Remove stale items (backwards)
for (int i = model->rowCount() - 1; i >= 0; --i) {
uint64_t id = model->item(i)->data(Qt::UserRole + 1).toULongLong();
if (!desiredMap.contains(id))
model->removeRow(i);
}
// Update existing items
QSet<uint64_t> existing;
for (int i = 0; i < model->rowCount(); ++i) {
auto* item = model->item(i);
uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
existing.insert(id);
auto dit = desiredMap.find(id);
if (dit == desiredMap.end()) continue;
const Entry& e = desired[*dit];
QString display = typeDisplayString(e.node, e.tree);
if (item->text() != display)
item->setText(display);
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
// Refresh children for structs
if (!e.isEnum)
buildStructChildren(item, e.tree, id, e.subPtr);
}
// Add new items
for (const auto& e : desired) {
if (existing.contains(e.id)) continue;
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
}
if (model->horizontalHeaderItem(0) == nullptr)
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
}
// ── Custom delegate for rich workspace tree rendering ──
class WorkspaceDelegate : public QStyledItemDelegate {
public:
using QStyledItemDelegate::QStyledItemDelegate;
void setThemeColors(const Theme& t) {
m_text = t.text;
m_textDim = t.textDim;
m_textMuted = t.textMuted;
m_syntaxType = t.syntaxType;
m_hover = t.hover;
m_selected = t.selected;
m_accent = t.borderFocused; // left accent bar
m_bg = t.background;
}
QSize sizeHint(const QStyleOptionViewItem& option,
const QModelIndex& index) const override {
QSize s = QStyledItemDelegate::sizeHint(option, index);
int pad = index.parent().isValid() ? 6 : 10;
s.setHeight(option.fontMetrics.height() + pad);
return s;
}
void paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const override {
painter->save();
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
opt.text.clear();
opt.icon = QIcon(); // we draw icon manually
QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter);
// Custom background for selection/hover
if (opt.state & QStyle::State_Selected) {
painter->fillRect(opt.rect, m_selected);
// Left accent bar
painter->fillRect(QRect(opt.rect.x(), opt.rect.y(), 2, opt.rect.height()), m_accent);
} else if (opt.state & QStyle::State_MouseOver) {
painter->fillRect(opt.rect, m_hover);
}
bool isChild = index.parent().isValid();
QString fullText = index.data(Qt::DisplayRole).toString();
QRect textRect = opt.rect.adjusted(4, 0, -4, 0);
// Draw icon for top-level items
if (!isChild) {
QVariant iconVar = index.data(Qt::DecorationRole);
if (iconVar.isValid()) {
QIcon icon = iconVar.value<QIcon>();
int iconSz = opt.fontMetrics.height();
int iconY = textRect.y() + (textRect.height() - iconSz) / 2;
icon.paint(painter, textRect.x(), iconY, iconSz, iconSz);
textRect.setLeft(textRect.left() + iconSz + 4);
}
}
painter->setFont(opt.font);
if (!isChild) {
// Top-level: "StructName (class) — 3"
int dashPos = fullText.indexOf(QChar(0x2014));
int parenPos = dashPos > 0 ? fullText.lastIndexOf(QStringLiteral(" ("), dashPos) : -1;
if (parenPos > 0) {
QString name = fullText.left(parenPos);
QString meta = fullText.mid(parenPos);
painter->setPen(m_text);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, name);
int nameW = opt.fontMetrics.horizontalAdvance(name);
QRect metaRect = textRect;
metaRect.setLeft(textRect.left() + nameW);
painter->setPen(m_textMuted);
painter->drawText(metaRect, Qt::AlignLeft | Qt::AlignVCenter, meta);
} else {
painter->setPen(m_text);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, fullText);
}
} else {
// Child: "TypeName fieldName"
int spacePos = fullText.indexOf(' ');
if (spacePos > 0) {
QString typeName = fullText.left(spacePos);
QString fieldName = fullText.mid(spacePos);
painter->setPen(m_syntaxType);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, typeName);
int typeW = opt.fontMetrics.horizontalAdvance(typeName);
QRect fieldRect = textRect;
fieldRect.setLeft(textRect.left() + typeW);
painter->setPen(m_textDim);
painter->drawText(fieldRect, Qt::AlignLeft | Qt::AlignVCenter, fieldName);
} else {
painter->setPen(m_textDim);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, fullText);
}
}
painter->restore();
}
private:
QColor m_text, m_textDim, m_textMuted, m_syntaxType;
QColor m_hover, m_selected, m_accent, m_bg;
};
} // namespace rcx