Skip to content

Text selection, refactors, code reviews - #48

Merged
frouaix merged 43 commits into
mainfrom
feature/textselection
Apr 30, 2026
Merged

frouaix merged 43 commits into
mainfrom
feature/textselection

Conversation

@frouaix

@frouaix frouaix commented Apr 27, 2026 •

Copy link
Copy Markdown
Owner

Text selection for text blocks
Code review (human and agent)

frouaix and others added 27 commits April 23, 2026 20:55
- Create packages/core/src/selection/types.ts with Selection, TextMetrics, CharacterBounds, TextLayout interfaces
- Create packages/core/src/selection/text-layout.ts with utility functions for text measurement, offset calculation, and character hit-testing
- Export new selection types and functions from core index
- Functions are reusable by both renderer (for display) and selection system (for hit-testing)
- Build and lint pass; no behavioral changes to existing rendering

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create packages/gui/src/selection/TextSelectionManager.ts
- Manage selections as Map<userId, Selection> for collaborative multi-user support
- API: setSelection, getSelection, getAllSelections, clearSelection, clearAllSelections
- Support configurable rendering with SelectionRenderConfig (colors, caret width)
- Export TextSelectionManager and SelectionRenderConfig from gui index
- Build and lint pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add handlePointerDown, handlePointerMove, handlePointerUp to TextSelectionManager
- Support drag-to-select with anchor/focus positions
- Track drag state to extend selection from initial click position to current pointer
- Integrate TextSelectionManager into VitrineComponent
- Add selectionConfig to VitrineComponentConfig for optional initialization
- Export getSelectionManager() on component for app-level access
- Build and lint pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add buildSelectionOverlays() to TextSelectionManager to generate Portal overlay blocks
- Render caret as thin vertical line using rectangle block
- Render selection highlight as translucent rectangle covering character range
- Support configurable colors per selection (for multi-user scenarios)
- Add CharacterBoundsProvider interface for layout query callback
- Integrate selection overlays into VitrineComponent buildBlock() render loop
- Overlays rendered as Portal blocks in group with main content
- Build and lint pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Export CharacterBoundsProvider type from GUI index
- TextSelectionManager fully integrated and exported
- SelectionRenderConfig configurable at VitrineComponent level
- Build and lint pass
- Foundation ready for event wiring and hit-testing integration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create packages/demo/text-selection.html with interactive selection testing
- Demonstrates multi-user selections with configurable colors
- Shows caret placement and drag-to-select functionality
- Includes controls to manually add selections and update stats
- Add text-selection.html to vite.config.ts build input
- Build and lint pass; demo accessible at /vitrine/text-selection.html

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…for VitrineComponent

VitrineComponent.mount() expects a container element (div) and creates
its own canvas internally. The demo was incorrectly passing a canvas
element, resulting in the canvas being created but not displayed.

Changed from <canvas id='canvas'> to <div id='canvas'> to allow
VitrineComponent to properly initialize the rendering surface.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ering

Provide a simple mock CharacterBoundsProvider that calculates character
bounds based on block position and assumed character width. This enables
the selection overlays (caret/highlight) to render when selections are
added through the manual controls.

Without this provider, buildSelectionOverlays() was returning null,
causing selections to be stored but not visible.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Change selectionColor opacity from 0.25 to 0.1 for range selections,
making the highlight boxes mostly transparent while still providing
clear visual indication of the selected text region.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make range selection highlights even more subtle for better visibility
of underlying text while maintaining clear visual feedback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pass explicit options to getContext('2d') with alpha: true to ensure
proper transparency handling, particularly for selection highlights
with rgba colors. Fixes opacity rendering in Safari.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…olors

When user picks a color with the color picker, convert it from hex (#RRGGBB)
to rgba with 0.05 opacity for range selections. This allows users to pick
the highlight color while maintaining proper transparency.

For caret selections (anchor === focus), use the solid hex color.
For range selections, convert to rgba(R, G, B, 0.05) for subtle highlighting.

This fixes the issue where selection highlights appeared fully opaque
instead of mostly transparent as intended.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Change range selection opacity from 0.05 (5%) to 0.2 (20%) for better
visibility of highlights while maintaining good text readability.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace hardcoded character width (10px) with proper canvas text metrics
measurement. Now measures each character's actual width using
canvas.measureText() to accurately position selection highlight boundaries.

This fixes the issue where selection highlight end position fell in the
middle of letters instead of at character boundaries, especially for
variable-width fonts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement hit-testing methods for finding character index from coordinates:
- hitTestBlockCharacter(): search for character at x,y in specific block
- Uses CharacterBoundsProvider to find closest character match
- Supports binary or linear search strategies

Implement keyboard event handling:
- handleKeyDown(): process arrow keys, Home/End, Ctrl+A
- Arrow keys: move caret or extend selection with Shift
- Home/End: jump to line/text boundaries
- Ctrl+A: select all text in block

These methods are designed to be called by VitrineComponent's event handlers
after coordinate transformation from screen space to canvas space.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add getCanvasCoordinates() to VitrineComponent for proper pixelRatio handling
- Wire pointer events (down/move/up) to TextSelectionManager.handlePointer*
- Wire keyboard events (arrows, shift+arrow, home/end) to TextSelectionManager.handleKeyDown()
- Make canvas focusable with tabindex for keyboard input
- Coordinate transformation: screen → canvas buffer (accounting for pixelRatio and display scaling)
- Hit-testing for determining character at click position
- Multi-user selection support now fully interactive

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fixed variable naming in getCanvasCoordinates()
- Now correctly uses canvas.width/height and rect.width/height
- Applies proper scaling from display coords to buffer coords
- Matches EventManager's coordinate transformation algorithm

Also added:
- Enhanced debug demo page (text-selection-debug.html) with 10 text blocks
- Multiple text configurations for testing (different fonts, sizes, wrapping)
- Debug output showing coordinate transformation chain

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend VitrineComponent pointer handlers to test blocks text1-text10
instead of just text1-text2. This allows the debug page to work with
all 10 test text blocks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Created text-selection-minimal.html for focused testing of coordinate
transformation and character index calculation. Single text block with
expected character indices for validation.

Includes:
- Minimal single-text-block setup
- Detailed debug output showing full coordinate transformation chain
- Expected vs actual character indices
- Clear instructions for manual testing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Changed from new VitrineComponent() with dx/dy config to VitrineComponent.block()
with width/height config, matching the working text-selection.html pattern.

This ensures text renders correctly in both minimal and debug test pages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CRITICAL FIX: CharacterBoundsProvider was returning bounds in scene
coordinates but hit-testing was done in canvas BUFFER coordinates.
With pixelRatio scaling (e.g., 2x), these don't match, causing all
hit-tests to fail.

Solution: Calculate pixelRatio dynamically and scale all bounds
returned by CharacterBoundsProvider:
- x = (sceneX + widthBefore) * scale
- y = sceneY * scale
- width = charWidth * scale
- height = lineHeight * scale

Applied to:
- text-selection-minimal.html (single text block test)
- text-selection-debug.html (multiple variant test)
- text-selection.html (main demo)

This is the root cause of selection placement not working.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Added console.log statements to see:
- When carets are being rendered with their coordinates
- How many overlay blocks are created

Also increased default caretWidth from 1 to 2 pixels for better visibility.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Improve text selection behavior across pointer and keyboard flows,
including dynamic selectable block discovery, insertion-based caret
placement, multiline range overlay rendering, and vertical arrow
navigation across wrapped lines.

Also harden and expand demo pages for regression testing with wrapped,
baseline-varied, and multi-style text blocks, and align demo bounds
providers with renderer behavior for more accurate hit-testing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add reusable character-bounds adapters and core text layout helpers,
refactor selection manager internals for readability, and cache insertion
geometry to reduce repeated scans.

Also remove hardcoded hit-test limits, gate library debug logging behind
config, auto-focus canvas for keyboard navigation, and simplify demo
selection wiring to use shared adapter utilities.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Relocate shared text-layout utilities and selection-related types from
packages/core/src/selection into packages/core/src/core, and update
imports/exports accordingly.

Also keep renderer text layout usage aligned with the new core module
path to reduce cross-directory fragmentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor text selection demo pages to use extracted CSS and split scene/setup
modules per demo, reducing inline HTML script/style complexity.

Also checkpoint selection metric fixes by isolating selection measurement
context and aligning text metric fallbacks toward stable line-box behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Include text-selection-debug.html in the bottom-page demo navigation ring,
while keeping other text-selection demo pages outside that ring.

Also add documentation for configuring text selection on regular text()
blocks, including setup, required block IDs, coordinate-space contract,
automatic provider behavior, and customization path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 27, 2026 00:12

Copilot AI left a comment

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.

Pull request overview

This PR strengthens and exposes regular text() selection capabilities across the core + GUI layers, refactors the demo pages into reusable modules/styles, and adds integration documentation for consumers.

Changes:

  • Add a GUI-side TextSelectionManager plus a block-tree adapter that derives per-character bounds from rendered text() blocks.
  • Centralize renderer-compatible text measurement/layout helpers in core (text-layout.ts) and export selection/layout types from vitrine.
  • Add/refactor multiple text-selection demos and document how to enable selection for regular text() blocks.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
test-results/.last-run.json Adds a last-run test status artifact.
packages/gui/src/selection/character-bounds-adapter.ts New adapter to build a CharacterBoundsProvider from a block tree using core text layout helpers.
packages/gui/src/selection/TextSelectionManager.ts New selection state manager: caret/range overlays, pointer drag selection, keyboard navigation.
packages/gui/src/index.ts Exports the new selection manager + adapter from vitrine-gui.
packages/gui/src/component.ts Integrates selection into VitrineComponent (events, focus/keydown handling, overlay composition).
packages/demo/vite.config.ts Registers new text-selection demo HTML entry points.
packages/demo/text-selection.html New full-feature text selection demo page.
packages/demo/text-selection.css Styling for the main text selection demo.
packages/demo/text-selection-setup.js Demo bootstrap + UI controls for multi-user selections.
packages/demo/text-selection-scene.js Demo scene construction for text selection.
packages/demo/text-selection-minimal.html Minimal click/hit-test validation page.
packages/demo/text-selection-minimal.css Styling for minimal test page.
packages/demo/text-selection-minimal-setup.js Minimal demo hit-test logging code.
packages/demo/text-selection-minimal-scene.js Minimal demo scene definition.
packages/demo/text-selection-debug.html Debug-focused selection demo page.
packages/demo/text-selection-debug.css Styling for debug demo.
packages/demo/text-selection-debug-setup.js Debug demo bootstrap + click logging.
packages/demo/text-selection-debug-scene.js Debug scene with many varied text() blocks (wrap, baseline, fonts).
packages/demo/gallery.html Updates gallery nav ring to include the new debug demo.
packages/demo/component-demo.html Updates nav to link to text selection debug page.
packages/core/src/index.ts Exports selection/layout types + text layout helpers from core.
packages/core/src/core/text-layout.ts New shared text measurement/layout utilities used by renderer and selection.
packages/core/src/core/selection-types.ts New exported selection/layout-related types.
packages/core/src/core/renderer-immediate.ts Refactors text outline + hit-test bounds to use shared core text layout helpers; tweaks getContext options.
packages/core/src/core/context.ts Improves measureText ascent/descent fallback behavior.
docs/TEXT_SELECTION_REGULAR_TEXT.md New docs on enabling selection for regular text() blocks in VitrineComponent.

Comment thread packages/gui/src/selection/character-bounds-adapter.ts Outdated
Comment thread packages/core/src/core/text-layout.ts Outdated
Comment thread test-results/.last-run.json Outdated
Comment thread packages/gui/src/component.ts
Comment on lines +335 to +340
const { left: xwCanvas, top: ywCanvas } = this.canvas.getBoundingClientRect();
const { clientX: xwPointer, clientY: ywPointer } = e;

return {
x: xwPointer - xwCanvas,
y: ywPointer - ywCanvas

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

getCanvasCoordinates() returns coordinates in CSS pixels relative to the displayed canvas element, but selection hit-testing uses scene/logical coordinates. If the canvas is scaled via CSS (e.g. canvas { width: 100% } in the demos), hit-testing will be offset/scaled incorrectly. Convert from CSS px to logical coords using the canvas bounding rect and the configured logical size (or use the same conversion logic as EventManager: css → buffer → logical).

Suggested change
const { left: xwCanvas, top: ywCanvas } = this.canvas.getBoundingClientRect();
const { clientX: xwPointer, clientY: ywPointer } = e;
return {
x: xwPointer - xwCanvas,
y: ywPointer - ywCanvas
const rwCanvas = this.canvas.getBoundingClientRect();
const { left: xwCanvas, top: ywCanvas, width: wwCanvas, height: hwCanvas } = rwCanvas;
if (wwCanvas === 0 || hwCanvas === 0) return null;
const { clientX: xwPointer, clientY: ywPointer } = e;
const xwCss = xwPointer - xwCanvas;
const ywCss = ywPointer - ywCanvas;
return {
x: (xwCss * this.canvas.width) / wwCanvas,
y: (ywCss * this.canvas.height) / hwCanvas

Copilot uses AI. Check for mistakes.
Comment thread packages/gui/src/selection/TextSelectionManager.ts Outdated
Comment thread packages/gui/src/selection/TextSelectionManager.ts Outdated
Comment thread packages/core/src/core/text-layout.ts
Comment thread packages/core/src/core/text-layout.ts
Comment thread packages/gui/src/component.ts
frouaix and others added 11 commits April 26, 2026 17:23
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
- keep a stable character-bounds provider instance across frames
- add lazy, per-block bounds computation with layout/world signatures
- invalidate insertion geometry only for changed blocks
- add empty-text guards in text layout and selection pointer handling

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- add extensible custom block registry in core for render/hit-test/bounds hooks
- route non-core block handling through registry in renderer and hit tester
- remove texta-specific types and runtime dependencies from core
- move texta block factory and registration helper to texta package
- update demos to register texta block type explicitly

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 65 out of 67 changed files in this pull request and generated 8 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread packages/gui/src/component.ts Outdated
Comment thread packages/gui/src/selection/character-bounds-adapter.ts Outdated
Comment thread packages/core/src/core/bounds.ts Outdated
Comment thread packages/core/src/core/text-layout.ts
Comment thread packages/gui/src/component.ts Outdated
Comment thread packages/gui/src/component.ts Outdated
Comment thread packages/core/src/core/context.ts
Comment thread packages/core/src/core/context.ts
frouaix and others added 2 commits April 29, 2026 19:32
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 65 out of 67 changed files in this pull request and generated 7 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread packages/demo/text-selection-debug-scene.js Outdated
Comment thread packages/demo/text-selection-debug-scene.js Outdated
Comment thread packages/demo/text-selection-debug-scene.js
Comment thread packages/core/src/core/text-layout.ts
Comment thread packages/gui/src/selection/character-bounds-adapter.ts Outdated
Comment thread packages/gui/src/selection/TextSelectionManager.ts
Comment thread packages/gui/src/component.ts
Co-authored-by: Copilot <copilot@github.com>

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 65 out of 67 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +224 to +233
let missStreak = 0;
const maxProbe = 20000;

for (let index = 0; index <= maxProbe; index++) {
const bounds = this.queryInsertionBounds(blockId, index);
if (!bounds) {
missStreak++;
if (missStreak >= 8) {
break;
}

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

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

maxProbe = 20000 hard-limits how many insertion indices can be discovered for a text block. For any selectable text longer than 20k chars, selection geometry will truncate and keyboard navigation / range rendering can’t reach the tail. Consider removing the hard cap (rely on the miss-streak termination), or making the probe limit configurable and/or derived from a known text length.

Copilot uses AI. Check for mistakes.
Comment on lines +288 to +307
// TODO: this is brute force search, should be optimized
private groupInsertionPointsByLine(points: InsertionPoint[], lineTolerance: number = 0.5): LineGroup[] {
const lineGroups: LineGroup[] = [];
for (const point of points) {
const lineGroup = lineGroups.find((line) =>
Math.abs(line.y - point.y) <= lineTolerance
&& Math.abs(line.height - point.height) <= lineTolerance
);
if (lineGroup) {
lineGroup.points.push({ index: point.index, x: point.x });
continue;
}
lineGroups.push({
y: point.y,
height: point.height,
points: [{ index: point.index, x: point.x }]
});
}
return lineGroups;
}

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

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

groupInsertionPointsByLine() does an O(n·lines) search (lineGroups.find(...)) for every insertion point and then later sorts again per line group. This can become a noticeable hot path for long text blocks or frequent re-rendering while dragging selections. A more scalable approach is to bucket by a quantized y/height key (Map) or pre-sort points by y and group in a single pass, avoiding repeated linear scans and per-group sorts.

Copilot uses AI. Check for mistakes.
Comment thread packages/core/src/core/context.ts
@frouaix frouaix changed the title Text selection hardening, demo refactor, and integration docs Text selection, refactors, code reviews Apr 30, 2026
@frouaix
frouaix merged commit b569a10 into main Apr 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants