@@ -65,8 +65,10 @@ export const DEFAULT_THEME: Required<ITheme> = {
6565 background : '#1e1e1e' ,
6666 cursor : '#ffffff' ,
6767 cursorAccent : '#1e1e1e' ,
68- selectionBackground : 'rgba(255, 255, 255, 0.3)' ,
69- selectionForeground : '#d4d4d4' ,
68+ // Selection colors: solid colors that replace cell bg/fg when selected
69+ // Using Ghostty's approach: selection bg = default fg, selection fg = default bg
70+ selectionBackground : '#d4d4d4' ,
71+ selectionForeground : '#1e1e1e' ,
7072 black : '#000000' ,
7173 red : '#cd3131' ,
7274 green : '#0dbc79' ,
@@ -112,8 +114,15 @@ export class CanvasRenderer {
112114 // Current buffer being rendered (for grapheme lookups)
113115 private currentBuffer : IRenderable | null = null ;
114116
115- // Selection manager (for rendering selection overlay )
117+ // Selection manager (for rendering selection)
116118 private selectionManager ?: SelectionManager ;
119+ // Cached selection coordinates for current render pass (viewport-relative)
120+ private currentSelectionCoords : {
121+ startCol : number ;
122+ startRow : number ;
123+ endCol : number ;
124+ endRow : number ;
125+ } | null = null ;
117126
118127 // Link rendering state
119128 private hoveredHyperlinkId : number = 0 ;
@@ -319,13 +328,15 @@ export class CanvasRenderer {
319328 const hasSelection = this . selectionManager && this . selectionManager . hasSelection ( ) ;
320329 const selectionRows = new Set < number > ( ) ;
321330
331+ // Cache selection coordinates for use during cell rendering
332+ // This is used by isInSelection() to determine if a cell needs selection colors
333+ this . currentSelectionCoords = hasSelection ? this . selectionManager ! . getSelectionCoords ( ) : null ;
334+
322335 // Mark current selection rows for redraw (includes programmatic selections)
323- if ( hasSelection ) {
324- const coords = this . selectionManager ! . getSelectionCoords ( ) ;
325- if ( coords ) {
326- for ( let row = coords . startRow ; row <= coords . endRow ; row ++ ) {
327- selectionRows . add ( row ) ;
328- }
336+ if ( this . currentSelectionCoords ) {
337+ const coords = this . currentSelectionCoords ;
338+ for ( let row = coords . startRow ; row <= coords . endRow ; row ++ ) {
339+ selectionRows . add ( row ) ;
329340 }
330341 }
331342
@@ -467,12 +478,8 @@ export class CanvasRenderer {
467478 }
468479 }
469480
470- // Render selection highlight AFTER all text (so it overlays)
471- // Only render if we actually rendered some lines
472- if ( hasSelection && anyLinesRendered ) {
473- // Draw selection overlay - only when we've redrawn the underlying text
474- this . renderSelection ( dims . cols ) ;
475- }
481+ // Selection highlighting is now integrated into renderCellBackground/renderCellText
482+ // No separate overlay pass needed - this fixes z-order issues with complex glyphs
476483
477484 // Link underlines are drawn during cell rendering (see renderCell)
478485
@@ -535,12 +542,24 @@ export class CanvasRenderer {
535542
536543 /**
537544 * Render a cell's background only (Pass 1 of two-pass rendering)
545+ * Selection highlighting is integrated here to avoid z-order issues with
546+ * complex glyphs (like Devanagari) that extend outside their cell bounds.
538547 */
539548 private renderCellBackground ( cell : GhosttyCell , x : number , y : number ) : void {
540549 const cellX = x * this . metrics . width ;
541550 const cellY = y * this . metrics . height ;
542551 const cellWidth = this . metrics . width * cell . width ;
543552
553+ // Check if this cell is selected
554+ const isSelected = this . isInSelection ( x , y ) ;
555+
556+ if ( isSelected ) {
557+ // Draw selection background (solid color, not overlay)
558+ this . ctx . fillStyle = this . theme . selectionBackground ;
559+ this . ctx . fillRect ( cellX , cellY , cellWidth , this . metrics . height ) ;
560+ return ; // Selection background replaces cell background
561+ }
562+
544563 // Extract background color and handle inverse
545564 let bg_r = cell . bg_r ,
546565 bg_g = cell . bg_g ,
@@ -564,6 +583,7 @@ export class CanvasRenderer {
564583
565584 /**
566585 * Render a cell's text and decorations (Pass 2 of two-pass rendering)
586+ * Selection foreground color is applied here to match the selection background.
567587 */
568588 private renderCellText ( cell : GhosttyCell , x : number , y : number ) : void {
569589 const cellX = x * this . metrics . width ;
@@ -575,26 +595,33 @@ export class CanvasRenderer {
575595 return ;
576596 }
577597
578- // Extract colors and handle inverse
579- let fg_r = cell . fg_r ,
580- fg_g = cell . fg_g ,
581- fg_b = cell . fg_b ;
582-
583- if ( cell . flags & CellFlags . INVERSE ) {
584- // When inverted, foreground becomes background
585- fg_r = cell . bg_r ;
586- fg_g = cell . bg_g ;
587- fg_b = cell . bg_b ;
588- }
598+ // Check if this cell is selected
599+ const isSelected = this . isInSelection ( x , y ) ;
589600
590601 // Set text style
591602 let fontStyle = '' ;
592603 if ( cell . flags & CellFlags . ITALIC ) fontStyle += 'italic ' ;
593604 if ( cell . flags & CellFlags . BOLD ) fontStyle += 'bold ' ;
594605 this . ctx . font = `${ fontStyle } ${ this . fontSize } px ${ this . fontFamily } ` ;
595606
596- // Set text color
597- this . ctx . fillStyle = this . rgbToCSS ( fg_r , fg_g , fg_b ) ;
607+ // Set text color - use selection foreground if selected
608+ if ( isSelected ) {
609+ this . ctx . fillStyle = this . theme . selectionForeground ;
610+ } else {
611+ // Extract colors and handle inverse
612+ let fg_r = cell . fg_r ,
613+ fg_g = cell . fg_g ,
614+ fg_b = cell . fg_b ;
615+
616+ if ( cell . flags & CellFlags . INVERSE ) {
617+ // When inverted, foreground becomes background
618+ fg_r = cell . bg_r ;
619+ fg_g = cell . bg_g ;
620+ fg_b = cell . bg_b ;
621+ }
622+
623+ this . ctx . fillStyle = this . rgbToCSS ( fg_r , fg_g , fg_b ) ;
624+ }
598625
599626 // Apply faint effect
600627 if ( cell . flags & CellFlags . FAINT ) {
@@ -816,9 +843,6 @@ export class CanvasRenderer {
816843 visibleRows : number ,
817844 opacity : number = 1
818845 ) : void {
819- // Don't render if fully transparent or no scrollback
820- if ( opacity <= 0 || scrollbackLength === 0 ) return ;
821-
822846 const ctx = this . ctx ;
823847 const canvasHeight = this . canvas . height / this . devicePixelRatio ;
824848 const canvasWidth = this . canvas . width / this . devicePixelRatio ;
@@ -829,6 +853,13 @@ export class CanvasRenderer {
829853 const scrollbarPadding = 4 ;
830854 const scrollbarTrackHeight = canvasHeight - scrollbarPadding * 2 ;
831855
856+ // Always clear the scrollbar area first (fixes ghosting when fading out)
857+ ctx . fillStyle = this . theme . background ;
858+ ctx . fillRect ( scrollbarX - 2 , 0 , scrollbarWidth + 6 , canvasHeight ) ;
859+
860+ // Don't draw scrollbar if fully transparent or no scrollback
861+ if ( opacity <= 0 || scrollbackLength === 0 ) return ;
862+
832863 // Calculate scrollbar thumb size and position
833864 const totalLines = scrollbackLength + visibleRows ;
834865 const thumbHeight = Math . max ( 20 , ( visibleRows / totalLines ) * scrollbarTrackHeight ) ;
@@ -859,12 +890,42 @@ export class CanvasRenderer {
859890 }
860891
861892 /**
862- * Set selection manager (for rendering selection overlay )
893+ * Set selection manager (for rendering selection)
863894 */
864895 public setSelectionManager ( manager : SelectionManager ) : void {
865896 this . selectionManager = manager ;
866897 }
867898
899+ /**
900+ * Check if a cell at (x, y) is within the current selection.
901+ * Uses cached selection coordinates for performance.
902+ */
903+ private isInSelection ( x : number , y : number ) : boolean {
904+ const sel = this . currentSelectionCoords ;
905+ if ( ! sel ) return false ;
906+
907+ const { startCol, startRow, endCol, endRow } = sel ;
908+
909+ // Single line selection
910+ if ( startRow === endRow ) {
911+ return y === startRow && x >= startCol && x <= endCol ;
912+ }
913+
914+ // Multi-line selection
915+ if ( y === startRow ) {
916+ // First line: from startCol to end of line
917+ return x >= startCol ;
918+ } else if ( y === endRow ) {
919+ // Last line: from start of line to endCol
920+ return x <= endCol ;
921+ } else if ( y > startRow && y < endRow ) {
922+ // Middle lines: entire line is selected
923+ return true ;
924+ }
925+
926+ return false ;
927+ }
928+
868929 /**
869930 * Set the currently hovered hyperlink ID for rendering underlines
870931 */
@@ -909,35 +970,6 @@ export class CanvasRenderer {
909970 this . ctx . fillRect ( 0 , 0 , this . canvas . width , this . canvas . height ) ;
910971 }
911972
912- /**
913- * Render selection overlay
914- */
915- private renderSelection ( cols : number ) : void {
916- const coords = this . selectionManager ! . getSelectionCoords ( ) ;
917- if ( ! coords ) return ;
918-
919- const { startCol, startRow, endCol, endRow } = coords ;
920-
921- // Use semi-transparent fill for selection
922- this . ctx . save ( ) ;
923- this . ctx . fillStyle = this . theme . selectionBackground ;
924- this . ctx . globalAlpha = 0.5 ; // Make it semi-transparent so text is visible
925-
926- for ( let row = startRow ; row <= endRow ; row ++ ) {
927- const colStart = row === startRow ? startCol : 0 ;
928- const colEnd = row === endRow ? endCol : cols - 1 ;
929-
930- const x = colStart * this . metrics . width ;
931- const y = row * this . metrics . height ;
932- const width = ( colEnd - colStart + 1 ) * this . metrics . width ;
933- const height = this . metrics . height ;
934-
935- this . ctx . fillRect ( x , y , width , height ) ;
936- }
937-
938- this . ctx . restore ( ) ;
939- }
940-
941973 /**
942974 * Cleanup resources
943975 */
0 commit comments