diff --git a/packages/app/src/terminal/native-renderer/colors.ts b/packages/app/src/terminal/native-renderer/colors.ts index 216f403f3..1ea663d6a 100644 --- a/packages/app/src/terminal/native-renderer/colors.ts +++ b/packages/app/src/terminal/native-renderer/colors.ts @@ -56,6 +56,11 @@ export interface TerminalCellStyleResolver { export const DEFAULT_TERMINAL_THEME = toXtermTheme(darkTheme.colors.terminal); +export const NATIVE_TERMINAL_SELECTION_COLORS = { + background: "#ffff00", + foreground: "#000000", +} as const; + function build256Palette(theme: ITheme): string[] { const palette = Array.from({ length: 256 }, () => ""); diff --git a/packages/app/src/terminal/native-renderer/terminal-grid-view.native.tsx b/packages/app/src/terminal/native-renderer/terminal-grid-view.native.tsx index b9f24f737..7cf3d20c8 100644 --- a/packages/app/src/terminal/native-renderer/terminal-grid-view.native.tsx +++ b/packages/app/src/terminal/native-renderer/terminal-grid-view.native.tsx @@ -11,7 +11,11 @@ import { import { StyleSheet } from "react-native-unistyles"; import type { ITheme } from "@xterm/xterm"; -import { createTerminalCellStyleResolver, DEFAULT_TERMINAL_THEME } from "./colors"; +import { + createTerminalCellStyleResolver, + DEFAULT_TERMINAL_THEME, + NATIVE_TERMINAL_SELECTION_COLORS, +} from "./colors"; import { resolveNativeTerminalFontFamily } from "./font.native"; import type { TerminalViewportState } from "./headless-terminal-state"; import { @@ -21,11 +25,7 @@ import { type TerminalGridCellMetrics, } from "./terminal-grid-metrics"; import { buildRows, type TerminalRowModel, type TerminalRun } from "./terminal-row-model"; -import { - resolveTerminalSelectionRects, - type TerminalSelectionRange, - type TerminalSelectionRect, -} from "./terminal-selection"; +import type { TerminalSelectionRange } from "./terminal-selection"; const MEASURE_TEXT = "mmmmmmmmmm"; const DEFAULT_FONT_SIZE = 12; @@ -58,11 +58,6 @@ interface TerminalGridRunProps { textStyle: StyleProp; } -interface TerminalGridSelectionRectProps { - rect: TerminalSelectionRect; - color: string; -} - export interface TerminalGridViewProps { state: TerminalViewportState; xtermTheme?: ITheme; @@ -130,27 +125,6 @@ const MemoTerminalGridRun = memo(TerminalGridRun, (previous, next) => { ); }); -function TerminalGridSelectionRect({ rect, color }: TerminalGridSelectionRectProps) { - const rectStyle = useMemo>( - () => [ - styles.selectionRect, - { - backgroundColor: color, - height: rect.height, - transform: [{ translateX: rect.x }, { translateY: rect.y }], - width: rect.width, - }, - ], - [color, rect.height, rect.width, rect.x, rect.y], - ); - - return ; -} - -const MemoTerminalGridSelectionRect = memo(TerminalGridSelectionRect, (previous, next) => { - return previous.rect === next.rect && previous.color === next.color; -}); - function TerminalGridRow({ row, cellWidth, @@ -244,23 +218,21 @@ export function TerminalGridView({ [state.grid, visibleCols], ); const rows = useMemo( - () => buildRows({ grid: projectedGrid, resolver }), - [projectedGrid, resolver], - ); - const selectionRects = useMemo( () => - resolveTerminalSelectionRects({ - selection, - viewport: { - firstRow: state.firstRow, - rows: state.grid.length, - cols: visibleCols, - }, - metrics, + buildRows({ + grid: projectedGrid, + resolver, + selection: selection + ? { + range: selection, + firstRow: state.firstRow, + backgroundColor: NATIVE_TERMINAL_SELECTION_COLORS.background, + foregroundColor: NATIVE_TERMINAL_SELECTION_COLORS.foreground, + } + : undefined, }), - [metrics, selection, state.firstRow, state.grid.length, visibleCols], + [projectedGrid, resolver, selection, state.firstRow], ); - const selectionColor = xtermTheme.selectionBackground ?? "rgba(90, 160, 255, 0.35)"; const containerStyle = useMemo>( () => [styles.root, { backgroundColor: resolver.backgroundColor }, style], @@ -346,9 +318,6 @@ export function TerminalGridView({ styleEpoch={resolver.themeKey} /> ))} - {selectionRects.map((rect) => ( - - ))} {!state.cursor.hidden && } @@ -384,10 +353,4 @@ const styles = StyleSheet.create({ position: "absolute", top: 0, }, - selectionRect: { - left: 0, - opacity: 0.8, - position: "absolute", - top: 0, - }, }); diff --git a/packages/app/src/terminal/native-renderer/terminal-row-model.test.ts b/packages/app/src/terminal/native-renderer/terminal-row-model.test.ts index d10fe6b36..c7962e56d 100644 --- a/packages/app/src/terminal/native-renderer/terminal-row-model.test.ts +++ b/packages/app/src/terminal/native-renderer/terminal-row-model.test.ts @@ -40,4 +40,43 @@ describe("terminal row model", () => { { text: "A界", cellCount: 3 }, ]); }); + + test("redraws selected cells with the terminal selection colors", () => { + const resolver = createTerminalCellStyleResolver(DEFAULT_TERMINAL_THEME); + const grid = [[cell("a", { fg: 1, fgMode: 1 }), cell("b", { dim: true }), cell("c")]]; + + const rows = buildRows({ + grid, + resolver, + selection: { + range: { + start: { row: 7, col: 1 }, + end: { row: 7, col: 2 }, + coordinateEpoch: 1, + }, + firstRow: 7, + backgroundColor: "#ffff00", + foregroundColor: "#000000", + }, + }); + + expect( + rows[0].runs.map((run) => ({ + text: run.text, + color: run.style.color, + backgroundColor: run.style.backgroundColor, + opacity: run.style.opacity, + })), + ).toEqual([ + { + text: "a", + color: DEFAULT_TERMINAL_THEME.red, + backgroundColor: undefined, + opacity: undefined, + }, + { text: "b", color: "#000000", backgroundColor: "#ffff00", opacity: 1 }, + { text: "c", color: "#000000", backgroundColor: "#ffff00", opacity: 1 }, + ]); + expect(rows[0].hash).not.toBe(buildRows({ grid, resolver })[0].hash); + }); }); diff --git a/packages/app/src/terminal/native-renderer/terminal-row-model.ts b/packages/app/src/terminal/native-renderer/terminal-row-model.ts index c490957a8..f814f934f 100644 --- a/packages/app/src/terminal/native-renderer/terminal-row-model.ts +++ b/packages/app/src/terminal/native-renderer/terminal-row-model.ts @@ -2,6 +2,7 @@ import type { TextStyle } from "react-native"; import type { TerminalCell } from "@getpaseo/protocol/messages"; import type { TerminalCellStyleResolver } from "./colors"; +import type { TerminalSelectionRange } from "./terminal-selection"; const WIDE_CHAR_RANGES: ReadonlyArray = [ [0x1100, 0x115f], @@ -32,6 +33,13 @@ export interface TerminalRowModel { type TerminalRenderableCell = TerminalCell & { width?: number }; +interface TerminalRowSelectionStyle { + range: TerminalSelectionRange; + firstRow: number; + backgroundColor: string; + foregroundColor: string; +} + function hashStringPart(hash: number, value: string): number { let nextHash = hash; for (let index = 0; index < value.length; index += 1) { @@ -93,10 +101,45 @@ function appendRun(input: { }); } +function cellIntersectsSelection(input: { + selection: TerminalRowSelectionStyle; + row: number; + col: number; + cellCount: number; +}): boolean { + const absoluteRow = input.selection.firstRow + input.row; + const { start, end } = input.selection.range; + if (absoluteRow < start.row || absoluteRow > end.row) { + return false; + } + + const selectedStartCol = absoluteRow === start.row ? start.col : 0; + const selectedEndCol = absoluteRow === end.row ? end.col : Number.POSITIVE_INFINITY; + const cellEndCol = input.col + input.cellCount - 1; + return cellEndCol >= selectedStartCol && input.col <= selectedEndCol; +} + +function selectedCellStyle(input: { + style: TextStyle; + foregroundColor: string; + backgroundColor: string; +}): TextStyle { + return { + ...input.style, + backgroundColor: input.backgroundColor, + color: input.foregroundColor, + opacity: 1, + ...(input.style.textDecorationLine + ? { textDecorationColor: input.foregroundColor } + : undefined), + }; +} + function buildRowModel(input: { cells: TerminalRenderableCell[]; index: number; resolver: TerminalCellStyleResolver; + selection?: TerminalRowSelectionStyle; }): TerminalRowModel { const runs: TerminalRun[] = []; let hash = 2166136261; @@ -106,17 +149,36 @@ function buildRowModel(input: { const text = cell.char || " "; const resolvedStyle = input.resolver.resolve(cell); const cellCount = terminalCellCount(input.cells, col, text); + const selection = input.selection; + let styleKey = resolvedStyle.key; + let style = resolvedStyle.style; + if ( + selection && + cellIntersectsSelection({ + selection, + row: input.index, + col, + cellCount, + }) + ) { + styleKey = `${resolvedStyle.key}|selection:${selection.foregroundColor}:${selection.backgroundColor}`; + style = selectedCellStyle({ + style: resolvedStyle.style, + foregroundColor: selection.foregroundColor, + backgroundColor: selection.backgroundColor, + }); + } appendRun({ runs, text, cellCount, - styleKey: resolvedStyle.key, - style: resolvedStyle.style, + styleKey, + style, col, }); hash = hashStringPart(hash, text); hash = hashStringPart(hash, String(cellCount)); - hash = hashStringPart(hash, resolvedStyle.key); + hash = hashStringPart(hash, styleKey); if (cellCount > 1) { col += 1; @@ -133,8 +195,9 @@ function buildRowModel(input: { export function buildRows(input: { grid: TerminalRenderableCell[][]; resolver: TerminalCellStyleResolver; + selection?: TerminalRowSelectionStyle; }): TerminalRowModel[] { return input.grid.map((cells, index) => - buildRowModel({ cells, index, resolver: input.resolver }), + buildRowModel({ cells, index, resolver: input.resolver, selection: input.selection }), ); }