Skip to content

feat: expose graphical value selection renderer hook#412

Merged
forntoh merged 1 commit into
feat/graphical-displayfrom
gd/07-graphical-value-selection-renderer
May 18, 2026
Merged

feat: expose graphical value selection renderer hook#412
forntoh merged 1 commit into
feat/graphical-displayfrom
gd/07-graphical-value-selection-renderer

Conversation

@forntoh
Copy link
Copy Markdown
Owner

@forntoh forntoh commented Apr 25, 2026

Summary

  • Extend GraphicalDisplayRenderer to implement and expose GraphicalValueSelectionRenderer via queryExtension().
  • Add value-selection state handling in the renderer draw path so editing overlays can highlight selected value substrings.
  • Document the renderer hook in graphical-display docs and add unit coverage in test/GraphicalDisplayRenderer.cpp to verify extension exposure.

Summary by CodeRabbit

  • New Features
    • Added visual selection highlighting support for graphical displays. Text input and character selection components now display an active selection indicator with a highlight box, providing clearer visual feedback when editing text on graphical display interfaces.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 278b1722-248e-472f-bd8e-1c67dfa6e59c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gd/07-graphical-value-selection-renderer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added documentation Improvements or additions to documentation feature New feature labels Apr 25, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/GraphicalDisplayRenderer.cpp`:
- Around line 233-241: The conditional block that forces selectionEnd =
selectionStart + 1 is unreachable given the invariants around hasValueSelection,
valueSelectionLength > 0 and valueLen >= 1; remove that recovery branch and
replace it with a short comment or an assert documenting the invariant
(referencing hasValueSelection, valueSelectionStart, valueSelectionLength,
safeLength(value), selectionStart, selectionEnd) so future readers know the
guard was intentionally omitted; alternatively, if you want to keep a defensive
check, convert it to an explicit assertion (e.g., assert(selectionEnd >
selectionStart)) with a comment explaining it exists solely to catch future API
changes that might allow zero-length selections.
- Around line 220-221: The code currently computes graphicalItem =
asGraphical(activeItem) and tightSelection = graphicalItem != NULL &&
graphicalItem->useTightGraphicalSelectionBox() on every drawItem call even
though tightSelection is only needed inside the selection-highlight branch; move
the lookup of useTightGraphicalSelectionBox() into the block guarded by hasFocus
&& MenuItem::isEditing() && hasValueSelection so you only call
graphicalItem->useTightGraphicalSelectionBox() when that branch runs, while
retaining a separate asGraphical(activeItem) lookup before the toggle branch
(the code at the toggle branch uses graphicalItem) or split into two lookups to
avoid changing behavior for the toggle path.

In `@test/GraphicalDisplayRenderer.cpp`:
- Around line 40-50: The test only covers the non-const queryExtension path; add
two assertions: call the const overload of
GraphicalDisplayRenderer::queryExtension (by using a const
GraphicalDisplayRenderer or const reference) with
GraphicalValueSelectionRenderer::extensionId() and assert the returned pointer
is non-null, and call queryExtension with a made-up/unknown extension id (e.g.,
a different integer or zero) and assert it returns NULL to ensure unknown ids
are rejected; update the
unittest(graphical_renderer_exposes_value_selection_extension) to perform these
two checks alongside the existing non-const code so both overloads and the
negative case are exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 49d43dcf-db49-4baa-9a50-79e319560e23

📥 Commits

Reviewing files that changed from the base of the PR and between 5c8bb40 and ceb4634.

📒 Files selected for processing (4)
  • docs/source/overview/rendering/graphical-display.rst
  • src/renderer/GraphicalDisplayRenderer.cpp
  • src/renderer/GraphicalDisplayRenderer.h
  • test/GraphicalDisplayRenderer.cpp

Comment on lines +220 to +221
const GraphicalMenuItem* graphicalItem = asGraphical(activeItem);
bool tightSelection = graphicalItem != NULL && graphicalItem->useTightGraphicalSelectionBox();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Optional: scope tightSelection to where it's used.

graphicalItem and tightSelection are computed on every drawItem call but only consumed inside the selection-highlight block at Line 256. Moving the lookup inside if (hasFocus && MenuItem::isEditing() && hasValueSelection) avoids querying useTightGraphicalSelectionBox() for every rendered row.

♻️ Suggested change
-    const GraphicalMenuItem* graphicalItem = asGraphical(activeItem);
-    bool tightSelection = graphicalItem != NULL && graphicalItem->useTightGraphicalSelectionBox();
-
     if (value != NULL && value[0] != '\0') {

…and inside the selection block:

         if (hasFocus && MenuItem::isEditing() && hasValueSelection) {
+            const GraphicalMenuItem* graphicalItem = asGraphical(activeItem);
+            bool tightSelection = graphicalItem != NULL && graphicalItem->useTightGraphicalSelectionBox();
             uint8_t valueLen = safeLength(value);

Note: graphicalItem is also used for the toggle branch at Line 277, so you'd need to keep the lookup for that path (or split the variable).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/GraphicalDisplayRenderer.cpp` around lines 220 - 221, The code
currently computes graphicalItem = asGraphical(activeItem) and tightSelection =
graphicalItem != NULL && graphicalItem->useTightGraphicalSelectionBox() on every
drawItem call even though tightSelection is only needed inside the
selection-highlight branch; move the lookup of useTightGraphicalSelectionBox()
into the block guarded by hasFocus && MenuItem::isEditing() && hasValueSelection
so you only call graphicalItem->useTightGraphicalSelectionBox() when that branch
runs, while retaining a separate asGraphical(activeItem) lookup before the
toggle branch (the code at the toggle branch uses graphicalItem) or split into
two lookups to avoid changing behavior for the toggle path.

Comment on lines +233 to +241
if (hasFocus && MenuItem::isEditing() && hasValueSelection) {
uint8_t valueLen = safeLength(value);
uint8_t selectionStart = valueSelectionStart > valueLen ? valueLen : valueSelectionStart;
uint16_t rawEnd = static_cast<uint16_t>(valueSelectionStart) + valueSelectionLength;
uint8_t selectionEnd = rawEnd > valueLen ? valueLen : static_cast<uint8_t>(rawEnd);

if (selectionEnd <= selectionStart && selectionStart < valueLen) {
selectionEnd = selectionStart + 1;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Defensive selectionEnd <= selectionStart expansion is unreachable.

Given the call site invariants (hasValueSelection is only true when valueSelectionLength > 0, and value[0] != '\0' so valueLen >= 1), tracing the clamp logic:

  • If valueSelectionStart < valueLen: selectionStart = valueSelectionStart and selectionEnd >= selectionStart + 1 (since length > 0), so selectionEnd > selectionStart always.
  • If valueSelectionStart >= valueLen: both clamp to valueLen, so selectionStart == valueLen and the second predicate selectionStart < valueLen is false.

Either the expansion block can be dropped, or the intent (e.g., guarding against future API changes that allow length = 0) deserves a brief comment so readers don't try to reason about which case it covers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/GraphicalDisplayRenderer.cpp` around lines 233 - 241, The
conditional block that forces selectionEnd = selectionStart + 1 is unreachable
given the invariants around hasValueSelection, valueSelectionLength > 0 and
valueLen >= 1; remove that recovery branch and replace it with a short comment
or an assert documenting the invariant (referencing hasValueSelection,
valueSelectionStart, valueSelectionLength, safeLength(value), selectionStart,
selectionEnd) so future readers know the guard was intentionally omitted;
alternatively, if you want to keep a defensive check, convert it to an explicit
assertion (e.g., assert(selectionEnd > selectionStart)) with a comment
explaining it exists solely to catch future API changes that might allow
zero-length selections.

Comment on lines +40 to +50
unittest(graphical_renderer_exposes_value_selection_extension) {
StubGraphicalDisplay display;
GraphicalDisplayRenderer renderer(&display);

void* extension = renderer.queryExtension(GraphicalValueSelectionRenderer::extensionId());
assertTrue(extension != NULL);

GraphicalValueSelectionRenderer* selection = static_cast<GraphicalValueSelectionRenderer*>(extension);
selection->setValueSelection(1, 2);
selection->clearValueSelection();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Optional: broaden coverage to const overload and negative case.

The test confirms the non-const queryExtension returns a usable pointer. Two small additions would lock down the contract more fully:

  1. Verify the const overload also returns non-null (it's a separate function in the diff).
  2. Verify an unknown extension id returns NULL, so a future regression that aliases ids is caught.
♻️ Suggested addition
     GraphicalValueSelectionRenderer* selection = static_cast<GraphicalValueSelectionRenderer*>(extension);
     selection->setValueSelection(1, 2);
     selection->clearValueSelection();
+
+    const GraphicalDisplayRenderer& constRenderer = renderer;
+    const void* constExtension = constRenderer.queryExtension(GraphicalValueSelectionRenderer::extensionId());
+    assertTrue(constExtension != NULL);
+
+    assertTrue(renderer.queryExtension(0xFF) == NULL);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
unittest(graphical_renderer_exposes_value_selection_extension) {
StubGraphicalDisplay display;
GraphicalDisplayRenderer renderer(&display);
void* extension = renderer.queryExtension(GraphicalValueSelectionRenderer::extensionId());
assertTrue(extension != NULL);
GraphicalValueSelectionRenderer* selection = static_cast<GraphicalValueSelectionRenderer*>(extension);
selection->setValueSelection(1, 2);
selection->clearValueSelection();
}
unittest(graphical_renderer_exposes_value_selection_extension) {
StubGraphicalDisplay display;
GraphicalDisplayRenderer renderer(&display);
void* extension = renderer.queryExtension(GraphicalValueSelectionRenderer::extensionId());
assertTrue(extension != NULL);
GraphicalValueSelectionRenderer* selection = static_cast<GraphicalValueSelectionRenderer*>(extension);
selection->setValueSelection(1, 2);
selection->clearValueSelection();
const GraphicalDisplayRenderer& constRenderer = renderer;
const void* constExtension = constRenderer.queryExtension(GraphicalValueSelectionRenderer::extensionId());
assertTrue(constExtension != NULL);
assertTrue(renderer.queryExtension(0xFF) == NULL);
}
🧰 Tools
🪛 Cppcheck (2.20.0)

[error] 40-40: syntax error

(syntaxError)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/GraphicalDisplayRenderer.cpp` around lines 40 - 50, The test only covers
the non-const queryExtension path; add two assertions: call the const overload
of GraphicalDisplayRenderer::queryExtension (by using a const
GraphicalDisplayRenderer or const reference) with
GraphicalValueSelectionRenderer::extensionId() and assert the returned pointer
is non-null, and call queryExtension with a made-up/unknown extension id (e.g.,
a different integer or zero) and assert it returns NULL to ensure unknown ids
are rejected; update the
unittest(graphical_renderer_exposes_value_selection_extension) to perform these
two checks alongside the existing non-const code so both overloads and the
negative case are exercised.

@github-actions
Copy link
Copy Markdown
Contributor

Memory usage change @ ceb4634

Board flash % RAM for global variables %
STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8 0 - 0 0.0 - 0.0 0 - 0 0.0 - 0.0
arduino:avr:uno 0 - 0 0.0 - 0.0 0 - 0 0.0 - 0.0
arduino:samd:mkr1000 0 - 0 0.0 - 0.0 0 - 0 0.0 - 0.0
esp32:esp32:esp32 🔺 0 - +628 0.0 - +0.05 🔺 0 - +8 0.0 - 0.0
esp8266:esp8266:huzzah N/A N/A N/A N/A
Click for full report table
Board examples/Basic
flash
% examples/Basic
RAM for global variables
% examples/ButtonAdapter
flash
% examples/ButtonAdapter
RAM for global variables
% examples/Callbacks
flash
% examples/Callbacks
RAM for global variables
% examples/InputRotary
flash
% examples/InputRotary
RAM for global variables
% examples/IntFloatValues
flash
% examples/IntFloatValues
RAM for global variables
% examples/KeyboardAdapter
flash
% examples/KeyboardAdapter
RAM for global variables
% examples/List
flash
% examples/List
RAM for global variables
% examples/SimpleRotary
flash
% examples/SimpleRotary
RAM for global variables
% examples/SSD1803A_I2C
flash
% examples/SSD1803A_I2C
RAM for global variables
% examples/UseByRef
flash
% examples/UseByRef
RAM for global variables
% examples/DynamicMenu
flash
% examples/DynamicMenu
RAM for global variables
% examples/Widgets
flash
% examples/Widgets
RAM for global variables
% examples/RTOS
flash
% examples/RTOS
RAM for global variables
% examples/ST7920_SPI
flash
% examples/ST7920_SPI
RAM for global variables
%
STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0
arduino:avr:uno 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0
arduino:samd:mkr1000 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0
esp32:esp32:esp32 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 628 0.05 8 0.0
esp8266:esp8266:huzzah N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A
Click for full report CSV
Board,examples/Basic<br>flash,%,examples/Basic<br>RAM for global variables,%,examples/ButtonAdapter<br>flash,%,examples/ButtonAdapter<br>RAM for global variables,%,examples/Callbacks<br>flash,%,examples/Callbacks<br>RAM for global variables,%,examples/InputRotary<br>flash,%,examples/InputRotary<br>RAM for global variables,%,examples/IntFloatValues<br>flash,%,examples/IntFloatValues<br>RAM for global variables,%,examples/KeyboardAdapter<br>flash,%,examples/KeyboardAdapter<br>RAM for global variables,%,examples/List<br>flash,%,examples/List<br>RAM for global variables,%,examples/SimpleRotary<br>flash,%,examples/SimpleRotary<br>RAM for global variables,%,examples/SSD1803A_I2C<br>flash,%,examples/SSD1803A_I2C<br>RAM for global variables,%,examples/UseByRef<br>flash,%,examples/UseByRef<br>RAM for global variables,%,examples/DynamicMenu<br>flash,%,examples/DynamicMenu<br>RAM for global variables,%,examples/Widgets<br>flash,%,examples/Widgets<br>RAM for global variables,%,examples/RTOS<br>flash,%,examples/RTOS<br>RAM for global variables,%,examples/ST7920_SPI<br>flash,%,examples/ST7920_SPI<br>RAM for global variables,%
STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0
arduino:avr:uno,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0
arduino:samd:mkr1000,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,,,,
esp32:esp32:esp32,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,0,0.0,628,0.05,8,0.0
esp8266:esp8266:huzzah,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,,,,,,,,

@forntoh forntoh enabled auto-merge (squash) May 18, 2026 20:33
@forntoh forntoh disabled auto-merge May 18, 2026 20:33
@forntoh forntoh merged commit c0768d1 into feat/graphical-display May 18, 2026
18 of 29 checks passed
@forntoh forntoh deleted the gd/07-graphical-value-selection-renderer branch May 18, 2026 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant