Text selection, refactors, code reviews - #48
Conversation
- 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>
There was a problem hiding this comment.
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
TextSelectionManagerplus a block-tree adapter that derives per-character bounds from renderedtext()blocks. - Centralize renderer-compatible text measurement/layout helpers in core (
text-layout.ts) and export selection/layout types fromvitrine. - 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. |
| const { left: xwCanvas, top: ywCanvas } = this.canvas.getBoundingClientRect(); | ||
| const { clientX: xwPointer, clientY: ywPointer } = e; | ||
|
|
||
| return { | ||
| x: xwPointer - xwCanvas, | ||
| y: ywPointer - ywCanvas |
There was a problem hiding this comment.
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).
| 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 |
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>
…trine into feature/textselection
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>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| // 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; | ||
| } |
There was a problem hiding this comment.
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.
Text selection for
textblocksCode review (human and agent)