diff --git a/src/compose.cpp b/src/compose.cpp index dbc3c25..c07607c 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -1,5 +1,6 @@ #include "core.h" #include +#include namespace rcx { @@ -146,6 +147,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree, lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field; lm.nodeKind = node.kind; lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont, state.offsetHexDigits); + lm.offsetAddr = tree.baseAddress + absAddr; lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth); lm.foldLevel = computeFoldLevel(depth, false); lm.effectiveTypeW = typeW; @@ -196,6 +198,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.depth = depth; lm.lineKind = LineKind::Field; lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits); + lm.offsetAddr = tree.baseAddress + absAddr; lm.nodeKind = node.kind; lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR); lm.foldLevel = computeFoldLevel(depth, false); @@ -213,6 +216,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.depth = depth; lm.lineKind = LineKind::ArrayElementSeparator; lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits); + lm.offsetAddr = tree.baseAddress + absAddr; lm.nodeKind = node.kind; lm.foldLevel = computeFoldLevel(depth, false); lm.markerMask = 0; @@ -241,6 +245,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.depth = depth; lm.lineKind = LineKind::Header; lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits); + lm.offsetAddr = tree.baseAddress + absAddr; lm.nodeKind = node.kind; lm.isRootHeader = false; lm.foldHead = true; @@ -303,6 +308,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.nodeKind = node.elementKind; lm.isArrayElement = true; lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + elemAddr, false, state.offsetHexDigits); + lm.offsetAddr = tree.baseAddress + elemAddr; lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth); lm.foldLevel = computeFoldLevel(childDepth, false); lm.effectiveTypeW = eTW; @@ -356,6 +362,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.markerMask = 0; int sz = tree.structSpan(node.id, &state.childMap); lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr + sz, false, state.offsetHexDigits); + lm.offsetAddr = tree.baseAddress + absAddr + sz; state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm); } @@ -388,6 +395,7 @@ void composeNode(ComposeState& state, const NodeTree& tree, lm.depth = depth; lm.lineKind = node.collapsed ? LineKind::Field : LineKind::Header; lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits); + lm.offsetAddr = tree.baseAddress + absAddr; lm.nodeKind = node.kind; lm.foldHead = true; lm.foldCollapsed = node.collapsed; @@ -586,6 +594,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR lm.foldLevel = SC_FOLDLEVELBASE; lm.foldHead = false; lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress, false, state.offsetHexDigits); + lm.offsetAddr = tree.baseAddress; lm.markerMask = 0; lm.effectiveTypeW = state.typeW; lm.effectiveNameW = state.nameW; @@ -604,7 +613,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR composeNode(state, tree, prov, idx, 0); } - return { state.text, state.meta, LayoutInfo{state.typeW, state.nameW, state.offsetHexDigits} }; + return { state.text, state.meta, LayoutInfo{state.typeW, state.nameW, state.offsetHexDigits, tree.baseAddress} }; } QSet NodeTree::normalizePreferAncestors(const QSet& ids) const { diff --git a/src/core.h b/src/core.h index 7794a22..294eca6 100644 --- a/src/core.h +++ b/src/core.h @@ -432,6 +432,7 @@ struct LineMeta { int arrayCount = 0; // Array: total element count int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element) QString offsetText; + uint64_t offsetAddr = 0; // Raw absolute address (for margin toggle) uint32_t markerMask = 0; bool dataChanged = false; // true if any byte in this node changed since last refresh QVector changedByteIndices; // Hex preview: which byte indices (0-based) changed on this line @@ -452,6 +453,7 @@ struct LayoutInfo { int typeW = 14; // Effective type column width (default = kColType) int nameW = 22; // Effective name column width (default = kColName) int offsetHexDigits = 8; // Hex digits for offset margin (4/8/12/16) + uint64_t baseAddress = 0; // Base address for relative offset computation }; // ── ComposeResult ── diff --git a/src/editor.cpp b/src/editor.cpp index df8b787..98a6b3e 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -26,6 +26,7 @@ static constexpr int IND_CMD_PILL = 12; // Rounded chip behind command row sp static constexpr int IND_DATA_CHANGED = 13; // Amber text for changed data values static constexpr int IND_CLASS_NAME = 14; // Teal text for root class name static constexpr int IND_HINT_GREEN = 15; // Green text for hint/comment text +static constexpr int IND_LOCAL_OFF = 16; // Dim text for inline local offset in relative mode static QString g_fontName = "JetBrains Mono"; @@ -171,6 +172,10 @@ void RcxEditor::setupScintilla() { m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_HINT_GREEN, 17 /*INDIC_TEXTFORE*/); + // Local offset text color (dim, like margin text) + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, + IND_LOCAL_OFF, 17 /*INDIC_TEXTFORE*/); + } void RcxEditor::setupLexer() { @@ -303,6 +308,8 @@ void RcxEditor::applyTheme(const Theme& theme) { IND_CLASS_NAME, theme.syntaxType); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, IND_HINT_GREEN, theme.indHintGreen); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, + IND_LOCAL_OFF, theme.textFaint); // Lexer colors m_lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::Keyword); @@ -409,6 +416,9 @@ void RcxEditor::applyDocument(const ComposeResult& result) { } void RcxEditor::applyMarginText(const QVector& meta) { + if (m_relativeOffsets) + return reformatMargins(); + m_sci->clearMarginText(-1); for (int i = 0; i < meta.size(); i++) { @@ -424,6 +434,107 @@ void RcxEditor::applyMarginText(const QVector& meta) { } } +void RcxEditor::reformatMargins() { + uint64_t base = m_layout.baseAddress; + int hexDigits = m_layout.offsetHexDigits; + + // ── Pass 1: margin text (global offset only) ── + m_sci->clearMarginText(-1); + for (int i = 0; i < m_meta.size(); i++) { + auto& lm = m_meta[i]; + + if (lm.isContinuation) { + lm.offsetText = QStringLiteral(" \u00B7 "); + } else if (lm.offsetText.isEmpty()) { + continue; + } else if (m_relativeOffsets) { + if (lm.lineKind == LineKind::Footer || + lm.lineKind == LineKind::ArrayElementSeparator || + lm.lineKind == LineKind::CommandRow) { + lm.offsetText = QString(hexDigits + 1, ' '); + } else { + uint64_t rel = lm.offsetAddr >= base ? lm.offsetAddr - base : 0; + lm.offsetText = (QStringLiteral("+") + + QString::number(rel, 16).toUpper()) + .rightJustified(hexDigits, ' ') + QChar(' '); + } + } else { + lm.offsetText = QString::number(lm.offsetAddr, 16).toUpper() + .rightJustified(hexDigits, '0') + QChar(' '); + } + + QByteArray text = lm.offsetText.toUtf8(); + m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETTEXT, + (uintptr_t)i, text.constData()); + QByteArray styles(text.size(), '\0'); + m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLES, + (uintptr_t)i, styles.constData()); + } + + // ── Pass 2: inline local offsets in the text indent area ── + m_sci->setReadOnly(false); + for (int i = 0; i < m_meta.size(); i++) { + const auto& lm = m_meta[i]; + if (lm.depth <= 1 || lm.isContinuation) continue; + if (lm.lineKind != LineKind::Field && lm.lineKind != LineKind::Header) + continue; + + // Place offset in the parent's indent slot (one level above the field's own indent) + // so the field's own 3-char indent acts as visual separator from the type column + int col = kFoldCol + (lm.depth - 2) * 3; + int slotWidth = 3; + + auto pos = [&](int c) -> long { + return m_sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN, + (unsigned long)i, (long)c); + }; + + if (m_relativeOffsets) { + // Derive local offset: find enclosing header or array element separator + uint64_t parentAddr = base; + for (int j = i - 1; j >= 0; j--) { + const auto& pLm = m_meta[j]; + if (pLm.lineKind == LineKind::Header && pLm.depth < lm.depth) { + parentAddr = pLm.offsetAddr; + break; + } + if (pLm.lineKind == LineKind::ArrayElementSeparator && pLm.depth <= lm.depth) { + parentAddr = pLm.offsetAddr; + break; + } + } + uint64_t localOff = lm.offsetAddr >= parentAddr ? lm.offsetAddr - parentAddr : 0; + + QString off = QStringLiteral("+") + + QString::number(localOff, 16).toUpper(); + QString padded = off.size() <= slotWidth + ? off.rightJustified(slotWidth, ' ') + : off; + long posA = pos(col); + long posB = pos(col + slotWidth); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, posA); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, posB); + QByteArray utf8 = padded.left(slotWidth).toUtf8(); + m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, + (uintptr_t)utf8.size(), utf8.constData()); + // Color the local offset dim + m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_LOCAL_OFF); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, + posA, posB - posA); + } else { + // Restore spaces when toggling off + long posA = pos(col); + long posB = pos(col + slotWidth); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, posA); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, posB); + QByteArray spaces(slotWidth, ' '); + m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, + (uintptr_t)spaces.size(), spaces.constData()); + } + } + m_sci->setReadOnly(true); +} + void RcxEditor::applyMarkers(const QVector& meta) { for (int m = M_CONT; m <= M_STRUCT_BG; m++) { m_sci->markerDeleteAll(m); @@ -1285,6 +1396,17 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { } return true; // consume release (prevent QScintilla from acting on it) } + // Double-click on offset margin → toggle absolute/relative + if (obj == m_sci->viewport() && event->type() == QEvent::MouseButtonDblClick) { + auto* me = static_cast(event); + int margin0Width = (int)m_sci->SendScintilla( + QsciScintillaBase::SCI_GETMARGINWIDTHN, 0UL, 0L); + if ((int)me->position().x() < margin0Width) { + m_relativeOffsets = !m_relativeOffsets; + reformatMargins(); + return true; + } + } // Double-click during edit mode: select entire editable text if (obj == m_sci->viewport() && m_editState.active && event->type() == QEvent::MouseButtonDblClick) { diff --git a/src/editor.h b/src/editor.h index 6d8a1ac..cd31b9d 100644 --- a/src/editor.h +++ b/src/editor.h @@ -77,6 +77,9 @@ private: QVector m_meta; LayoutInfo m_layout; // cached from ComposeResult + // ── Toggle: absolute vs relative offset margin + bool m_relativeOffsets = false; + int m_marginStyleBase = -1; int m_hintLine = -1; @@ -138,6 +141,7 @@ private: void allocateMarginStyles(); void applyMarginText(const QVector& meta); + void reformatMargins(); void applyMarkers(const QVector& meta); void applyFoldLevels(const QVector& meta); void applyHexDimming(const QVector& meta);