From 813552e41645cd400fb37b55ca20149c6fcffc1f Mon Sep 17 00:00:00 2001 From: Konstantin Hudyakov Date: Mon, 14 Jul 2025 15:05:08 +0300 Subject: [PATCH] [terminal] IJPL-196891 Paint cursor depending on cell foreground and background Like in the Classic Terminal. Previously, the cursor was always either black or white. Now it adapts to the symbol it is showing on. So, the cursor will always be visible on any background. GitOrigin-RevId: 7513a74d6fb45f329ab0df18aec1ddebf1d20c37 --- .../frontend/TerminalCursorPainter.kt | 89 +++++++++++-------- .../block/ui/BlockTerminalColorPalette.kt | 4 +- 2 files changed, 54 insertions(+), 39 deletions(-) diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalCursorPainter.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalCursorPainter.kt index da7dfcfe937f..15e9e88e51ec 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalCursorPainter.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalCursorPainter.kt @@ -7,7 +7,6 @@ import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.asContextElement import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.codePointAt -import com.intellij.openapi.editor.colors.EditorColors import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.ex.FocusChangeListener import com.intellij.openapi.editor.impl.EditorImpl @@ -17,19 +16,18 @@ import com.intellij.openapi.editor.markup.HighlighterTargetArea import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.terminal.TerminalUiSettingsManager -import com.intellij.ui.ColorUtil -import com.intellij.ui.Gray -import com.intellij.ui.JBColor import com.intellij.ui.scale.JBUIScale import com.intellij.util.asDisposable import com.intellij.util.concurrency.annotations.RequiresEdt import com.jediterm.terminal.CursorShape +import com.jediterm.terminal.ui.AwtTransformers import kotlinx.coroutines.* import org.jetbrains.plugins.terminal.TerminalOptionsProvider import org.jetbrains.plugins.terminal.TerminalUtil import org.jetbrains.plugins.terminal.block.reworked.TerminalOutputModel import org.jetbrains.plugins.terminal.block.reworked.TerminalOutputModelListener import org.jetbrains.plugins.terminal.block.reworked.TerminalSessionModel +import org.jetbrains.plugins.terminal.block.ui.BlockTerminalColorPalette import java.awt.Color import java.awt.Font import java.awt.Graphics2D @@ -136,13 +134,13 @@ internal class TerminalCursorPainter private constructor( val renderer = when (cursorShape) { CursorShape.BLINK_BLOCK, CursorShape.STEADY_BLOCK -> if (state.isFocused) { - BlockCursorRenderer(editor, listeners) + BlockCursorRenderer(editor, outputModel, listeners) } else { - EmptyBlockCursorRenderer(editor, listeners) + EmptyBlockCursorRenderer(editor, outputModel, listeners) } - CursorShape.BLINK_UNDERLINE, CursorShape.STEADY_UNDERLINE -> UnderlineCursorRenderer(editor, listeners) - CursorShape.BLINK_VERTICAL_BAR, CursorShape.STEADY_VERTICAL_BAR -> VerticalBarCursorRenderer(editor, listeners) + CursorShape.BLINK_UNDERLINE, CursorShape.STEADY_UNDERLINE -> UnderlineCursorRenderer(editor, outputModel, listeners) + CursorShape.BLINK_VERTICAL_BAR, CursorShape.STEADY_VERTICAL_BAR -> VerticalBarCursorRenderer(editor, outputModel, listeners) } if (shouldBlink) { paintBlinkingCursor(renderer, state.offset) @@ -215,30 +213,36 @@ internal class TerminalCursorPainter private constructor( private sealed class CursorRendererBase( private val editor: EditorEx, + private val outputModel: TerminalOutputModel, private val listeners: List, ) : CursorRenderer { private val grid: CharacterGrid = requireNotNull((editor as? EditorImpl)?.characterGrid) { "The editor is not in the grid mode" } - val editorCursorColor: Color - get() = editor.colorsScheme.getColor(EditorColors.CARET_COLOR) ?: JBColor(CURSOR_DARK, CURSOR_LIGHT) + /** Whether background color should be used as a foreground for text under the cursor */ + open val inverseForeground: Boolean = false - abstract val cursorForeground: Color? + abstract fun paintCursor(g: Graphics2D, rect: Rectangle2D.Double, color: Color) - abstract fun paintCursor(g: Graphics2D, rect: Rectangle2D.Double) - - protected inline fun Graphics2D.withCursorColor(block: () -> Unit) { + protected inline fun Graphics2D.withColor(color: Color, block: () -> Unit) { val oldColor = color try { - color = editorCursorColor + this.color = color block() } finally { - color = oldColor + this.color = oldColor } } final override fun installCursorHighlighter(offset: Int): RangeHighlighter { - val attributes = TextAttributes(cursorForeground, null, null, null, Font.PLAIN) + val cursorAttributes = getCursorTextAttributes(offset) + val colorPalette = BlockTerminalColorPalette() + val foregroundColor = cursorAttributes.foregroundColor ?: AwtTransformers.toAwtColor(colorPalette.defaultForeground)!! + val backgroundColor = cursorAttributes.backgroundColor ?: AwtTransformers.toAwtColor(colorPalette.defaultBackground)!! + + val effectiveForeground = if (inverseForeground) backgroundColor else foregroundColor + val attributes = TextAttributes(effectiveForeground, null, null, null, Font.PLAIN) + // offset == textLength is allowed (it means that the cursor is at the end, a very common case) val startOffset = offset.coerceIn(0..editor.document.textLength) val endOffset = (offset + 1).coerceIn(0..editor.document.textLength) @@ -253,7 +257,7 @@ internal class TerminalCursorPainter private constructor( val cursorHeight = editor.lineHeight val rect = Rectangle2D.Double(point.x, point.y, cursorWidth.toDouble(), cursorHeight.toDouble()) g as Graphics2D - paintCursor(g, rect) + paintCursor(g, rect, foregroundColor) for (listener in listeners) { listener.cursorPainted() @@ -262,17 +266,29 @@ internal class TerminalCursorPainter private constructor( return highlighter } + + private fun getCursorTextAttributes(offset: Int): TextAttributes { + val highlightings = outputModel.getHighlightings() + val highlightingIndex = highlightings.findHighlightingIndex(offset) + return if (highlightingIndex in 0 until highlightings.size) { + highlightings[highlightingIndex].textAttributesProvider.getTextAttributes() + } + else { + // Cursor can be at the end of the document. Use the default attributes in this case. + TextAttributes.ERASE_MARKER + } + } } private class BlockCursorRenderer( editor: EditorEx, + outputModel: TerminalOutputModel, listeners: List, - ) : CursorRendererBase(editor, listeners) { - override val cursorForeground: Color - get() = if (ColorUtil.isDark(editorCursorColor)) CURSOR_LIGHT else CURSOR_DARK + ) : CursorRendererBase(editor, outputModel, listeners) { + override val inverseForeground: Boolean = true - override fun paintCursor(g: Graphics2D, rect: Rectangle2D.Double) { - g.withCursorColor { + override fun paintCursor(g: Graphics2D, rect: Rectangle2D.Double, color: Color) { + g.withColor(color) { g.fill(rect) } } @@ -280,12 +296,11 @@ internal class TerminalCursorPainter private constructor( private class EmptyBlockCursorRenderer( editor: EditorEx, + outputModel: TerminalOutputModel, listeners: List, - ) : CursorRendererBase(editor, listeners) { - override val cursorForeground: Color? = null - - override fun paintCursor(g: Graphics2D, rect: Rectangle2D.Double) { - g.withCursorColor { + ) : CursorRendererBase(editor, outputModel, listeners) { + override fun paintCursor(g: Graphics2D, rect: Rectangle2D.Double, color: Color) { + g.withColor(color) { g.draw(rect) } } @@ -293,16 +308,15 @@ internal class TerminalCursorPainter private constructor( private abstract class LineCursorRenderer( editor: EditorEx, + outputModel: TerminalOutputModel, listeners: List, - ) : CursorRendererBase(editor, listeners) { - override val cursorForeground: Color? = null - + ) : CursorRendererBase(editor, outputModel, listeners) { protected val lineThickness: Double get() = JBUIScale.scale(2.0f).toDouble() protected abstract fun shape(rect: Rectangle2D.Double): Rectangle2D.Double - override fun paintCursor(g: Graphics2D, rect: Rectangle2D.Double) { - g.withCursorColor { + override fun paintCursor(g: Graphics2D, rect: Rectangle2D.Double, color: Color) { + g.withColor(color) { g.fill(shape(rect)) } } @@ -310,24 +324,23 @@ internal class TerminalCursorPainter private constructor( private class UnderlineCursorRenderer( editor: EditorEx, + outputModel: TerminalOutputModel, listeners: List, - ) : LineCursorRenderer(editor, listeners) { + ) : LineCursorRenderer(editor, outputModel, listeners) { override fun shape(rect: Rectangle2D.Double): Rectangle2D.Double = Rectangle2D.Double(rect.x, rect.y + rect.height - lineThickness, rect.width, lineThickness) } private class VerticalBarCursorRenderer( editor: EditorEx, + outputModel: TerminalOutputModel, listeners: List, - ) : LineCursorRenderer(editor, listeners) { + ) : LineCursorRenderer(editor, outputModel, listeners) { override fun shape(rect: Rectangle2D.Double): Rectangle2D.Double = Rectangle2D.Double(rect.x, rect.y, lineThickness, rect.height) } companion object { - private val CURSOR_LIGHT: Color = Gray._255 - private val CURSOR_DARK: Color = Gray._0 - @RequiresEdt fun install( editor: EditorEx, diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/ui/BlockTerminalColorPalette.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/ui/BlockTerminalColorPalette.kt index a586eac1b606..f6ab2ae88ff5 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/ui/BlockTerminalColorPalette.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/ui/BlockTerminalColorPalette.kt @@ -9,8 +9,10 @@ import com.intellij.terminal.BlockTerminalColors import com.intellij.terminal.TerminalColorPalette import com.jediterm.core.Color import com.jediterm.terminal.ui.AwtTransformers +import org.jetbrains.annotations.ApiStatus -internal class BlockTerminalColorPalette : TerminalColorPalette() { +@ApiStatus.Internal +class BlockTerminalColorPalette : TerminalColorPalette() { private val colorKeys = BlockTerminalColors.KEYS private val colorsScheme: EditorColorsScheme