Skip to content

Commit

Permalink
Commonize keyboard event handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
zhelenskiy committed Apr 14, 2024
1 parent 7612806 commit 41a2475
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 67 deletions.
34 changes: 16 additions & 18 deletions composeApp/src/commonMain/kotlin/CorrectBracketSequence.kt
Original file line number Diff line number Diff line change
Expand Up @@ -197,95 +197,93 @@ fun CorrectBracketSequence() {
)
)
},
keyEventHandler = combineKeyEventHandlers(
keyboardEventHandler = combineKeyboardEventHandlers(
handleMovingOffsets(
state = codeTextFieldState,
)
),
charEventHandler = combineCharEventHandlers(
),
reusingCharsEventHandler(
textFieldState = codeTextFieldState,
chars = "])>}",
),
openingBracketCharEventHandler(
openingBracketEventHandler(
textFieldState = codeTextFieldState,
openingChar = '[',
openingBracket = "[",
closingBracket = "]",
addNewLinesForSelection = { false },
),
closingBracketCharEventHandler(
closingBracketEventHandler(
textFieldState = codeTextFieldState,
openingBracket = "[",
closingBracket = "]",
closingChar = ']',
matchedBrackets = matchedBrackets,
),
openingBracketCharEventHandler(
openingBracketEventHandler(
textFieldState = codeTextFieldState,
openingChar = '(',
openingBracket = "(",
closingBracket = ")",
addNewLinesForSelection = { false },
),
closingBracketCharEventHandler(
closingBracketEventHandler(
textFieldState = codeTextFieldState,
openingBracket = "(",
closingBracket = ")",
closingChar = ')',
matchedBrackets = matchedBrackets,
),
openingBracketCharEventHandler(
openingBracketEventHandler(
textFieldState = codeTextFieldState,
openingChar = '<',
openingBracket = "<",
closingBracket = ">",
addNewLinesForSelection = { false },
),
closingBracketCharEventHandler(
closingBracketEventHandler(
textFieldState = codeTextFieldState,
openingBracket = "<",
closingBracket = ">",
closingChar = '>',
matchedBrackets = matchedBrackets,
),
openingBracketCharEventHandler(
openingBracketEventHandler(
textFieldState = codeTextFieldState,
openingChar = '{',
openingBracket = "{",
closingBracket = "}",
addNewLinesForSelection = { true },
),
closingBracketCharEventHandler(
closingBracketEventHandler(
textFieldState = codeTextFieldState,
openingBracket = "{",
closingBracket = "}",
closingChar = '}',
matchedBrackets = matchedBrackets,
),
newLineCharEventHandler(
newLineEventHandler(
textFieldState = codeTextFieldState,
matchedBrackets = matchedBrackets,
),
removeIndentBackspaceCharEventHandler(
removeIndentBackspaceEventHandler(
textFieldState = codeTextFieldState,
),
removeEmptyBracesBackspaceCharEventHandler(
removeEmptyBracesBackspaceEventHandler(
textFieldState = codeTextFieldState,
openingBracket = "[",
closingBracket = "]",
),
removeEmptyBracesBackspaceCharEventHandler(
removeEmptyBracesBackspaceEventHandler(
textFieldState = codeTextFieldState,
openingBracket = "(",
closingBracket = ")",
),
removeEmptyBracesBackspaceCharEventHandler(
removeEmptyBracesBackspaceEventHandler(
textFieldState = codeTextFieldState,
openingBracket = "<",
closingBracket = ">",
),
removeEmptyBracesBackspaceCharEventHandler(
removeEmptyBracesBackspaceEventHandler(
textFieldState = codeTextFieldState,
openingBracket = "{",
closingBracket = "}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,22 +141,24 @@ public data class EditorOffsets(
val end: Int = 0,
)

public typealias CharEventHandler = (CharEvent) -> TextFieldValue?
public typealias KeyboardEventHandler = (KeyboardEvent) -> TextFieldValue?
public typealias KeyboardEventFilter = (KeyboardEvent) -> Boolean

public fun combineCharEventHandlers(vararg handlers: CharEventHandler?): CharEventHandler =
public fun combineKeyboardEventHandlers(vararg handlers: KeyboardEventHandler?): KeyboardEventHandler =
{ event -> handlers.firstNotNullOfOrNull { it?.invoke(event) } }

public typealias KeyEventHandler = (KeyEvent) -> TextFieldValue?
public typealias KeyEventFilter = (KeyEvent) -> Boolean
public fun combineKeyboardEventFilters(vararg filters: KeyboardEventFilter?): KeyboardEventFilter =
{ event -> filters.any { it?.invoke(event) == true } }

public fun combineKeyEventHandlers(vararg handlers: KeyEventHandler?): KeyEventHandler =
{ event -> handlers.firstNotNullOfOrNull { it?.invoke(event) } }
public interface KeyboardEvent

public data class PhysicalKeyboardEvent(val keyEvent: KeyEvent) : KeyboardEvent

public sealed class CharEvent {
public data object NonTextEvent : CharEvent()
public data class Insert(val char: Char) : CharEvent()
public data object Backspace : CharEvent()
public data object Misc : CharEvent()
public sealed class UniversalKeyboardEvent : KeyboardEvent {
public data object NonTextEvent : UniversalKeyboardEvent()
public data class Insert(val char: Char) : UniversalKeyboardEvent()
public data object Backspace : UniversalKeyboardEvent()
public data object Misc : UniversalKeyboardEvent()
}

private fun <T : Token> isBackSpace(
Expand Down Expand Up @@ -238,33 +240,33 @@ public fun <T : Token> initializeBasicSourceCodeTextFieldState(
textFieldState: TextFieldValue,
preprocessors: List<Preprocessor>,
tokenize: Tokenizer<T>,
charEventHandler: CharEventHandler,
keyboardEventHandler: KeyboardEventHandler,
): BasicSourceCodeTextFieldState<T> = translate(
textFieldState = textFieldState,
preprocessors = preprocessors,
tokenize = tokenize,
state = BasicSourceCodeTextFieldState(),
charEventHandler = charEventHandler,
keyboardEventHandler = keyboardEventHandler,
)

private fun <T : Token> translate(
textFieldState: TextFieldValue,
preprocessors: List<Preprocessor>,
tokenize: Tokenizer<T>,
state: BasicSourceCodeTextFieldState<T>,
charEventHandler: CharEventHandler,
keyboardEventHandler: KeyboardEventHandler,
): BasicSourceCodeTextFieldState<T> {
val preprocessed =
preprocessors.fold(textFieldState) { acc, preprocessor -> preprocessor(acc) }
val charEvent = when {
state.text == textFieldState.text -> CharEvent.NonTextEvent
isBackSpace(state, textFieldState) -> CharEvent.Backspace
state.text == textFieldState.text -> UniversalKeyboardEvent.NonTextEvent
isBackSpace(state, textFieldState) -> UniversalKeyboardEvent.Backspace
isCharInserted(state, textFieldState) ->
CharEvent.Insert(char = textFieldState.text[textFieldState.selection.start - 1])
UniversalKeyboardEvent.Insert(char = textFieldState.text[textFieldState.selection.start - 1])

else -> CharEvent.Misc
else -> UniversalKeyboardEvent.Misc
}
val handledAsCharEvent = charEventHandler(charEvent) ?: preprocessed
val handledAsCharEvent = keyboardEventHandler(charEvent) ?: preprocessed
return tokenize(handledAsCharEvent)
}

Expand Down Expand Up @@ -305,8 +307,7 @@ public fun <T : Token> BasicSourceCodeTextField(
lineNumberModifier: Modifier = defaultLineNumberModifier,
editorOffsetsForPosition: (sourceCodePosition: SourceCodePosition) -> EditorOffsets = { EditorOffsets() },
manualScrollToPosition: SharedFlow<SourceCodePosition> = remember { MutableSharedFlow() },
charEventHandler: CharEventHandler = { null },
keyEventHandler: KeyEventHandler = { null },
keyboardEventHandler: KeyboardEventHandler = { null },
onHoveredSourceCodePositionChange: (position: SourceCodePosition) -> Unit = {},
horizontalThresholdEdgeChars: Int = 5,
verticalThresholdEdgeLines: Int = 1,
Expand Down Expand Up @@ -451,7 +452,7 @@ public fun <T : Token> BasicSourceCodeTextField(
var textFieldSize: IntSize? by remember { mutableStateOf(null) }

fun translate(textFieldState: TextFieldValue) = translate(
textFieldState, preprocessors, tokenize, state, charEventHandler
textFieldState, preprocessors, tokenize, state, keyboardEventHandler
)

fun onValueChange(newTextFieldState: TextFieldValue) {
Expand Down Expand Up @@ -496,7 +497,7 @@ public fun <T : Token> BasicSourceCodeTextField(
)
.onSizeChanged { textFieldSize = it }
.onPreviewKeyEvent {
when (val eventResult = keyEventHandler(it)) {
when (val eventResult = keyboardEventHandler(PhysicalKeyboardEvent(it))) {
null -> false
else -> {
onValueChange(eventResult)
Expand Down
37 changes: 30 additions & 7 deletions editor/src/commonMain/kotlin/editor/basic/KeyEventHandlers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,44 @@ package editor.basic
import androidx.compose.ui.input.key.*
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import kotlin.jvm.JvmName

public typealias PhysicalKeyboardEventHandler = (PhysicalKeyboardEvent) -> TextFieldValue?
public typealias PhysicalKeyboardEventFilter = (PhysicalKeyboardEvent) -> Boolean

public fun PhysicalKeyboardEventHandler.asKeyboardEventHandler(): KeyboardEventHandler = {
if (it is PhysicalKeyboardEvent) invoke(it) else null
}

public fun PhysicalKeyboardEventFilter.asKeyboardEventFilter(): KeyboardEventFilter = {
it is PhysicalKeyboardEvent && invoke(it)
}

public typealias ComposeKeyEventHandler = (KeyEvent) -> TextFieldValue?
public typealias ComposeKeyEventFilter = (KeyEvent) -> Boolean

@JvmName("composeKeyboardEventHandlerAsKeyboardEventHandler")
public fun ComposeKeyEventHandler.asKeyboardEventHandler(): KeyboardEventHandler =
{ event: PhysicalKeyboardEvent -> invoke(event.keyEvent) }.asKeyboardEventHandler()

@JvmName("composeKeyboardEventFilterAsKeyboardEventFilter")
public fun ComposeKeyEventFilter.asKeyboardEventFilter(): KeyboardEventFilter =
{ event: PhysicalKeyboardEvent -> invoke(event.keyEvent) }.asKeyboardEventFilter()

public fun <T : Token> handleMovingOffsets(
state: BasicSourceCodeTextFieldState<T>,
indent: String = " ".repeat(4),
moveForwardFilter: KeyEventFilter = { keyEvent ->
moveForwardFilter: KeyboardEventFilter = { keyEvent: KeyEvent ->
!keyEvent.isShiftPressed && keyEvent.key == Key.Tab && keyEvent.type == KeyEventType.KeyDown &&
!keyEvent.isAltPressed && !keyEvent.isCtrlPressed && !keyEvent.isMetaPressed
},
moveBackwardFilter: KeyEventFilter = { keyEvent ->
}.asKeyboardEventFilter(),
moveBackwardFilter: KeyboardEventFilter = { keyEvent: KeyEvent ->
keyEvent.isShiftPressed && keyEvent.key == Key.Tab && keyEvent.type == KeyEventType.KeyDown &&
!keyEvent.isAltPressed && !keyEvent.isCtrlPressed && !keyEvent.isMetaPressed
},
): KeyEventHandler = f@{ keyEvent: KeyEvent ->
val moveForward = !state.selection.collapsed && moveForwardFilter(keyEvent)
val moveBackward = moveBackwardFilter(keyEvent)
}.asKeyboardEventFilter(),
): KeyboardEventHandler = f@{ keyboardEvent: KeyboardEvent ->
val moveForward = !state.selection.collapsed && moveForwardFilter(keyboardEvent)
val moveBackward = moveBackwardFilter(keyboardEvent)
if (moveBackward == moveForward) return@f null
val selectionLines =
state.sourceCodePositions[state.selection.min].line..state.sourceCodePositions[state.selection.max].line
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,24 @@ package editor.basic
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue

public typealias UniversalKeyboardEventHandler = (UniversalKeyboardEvent) -> TextFieldValue?
public typealias UniversalKeyboardEventFilter = (UniversalKeyboardEvent) -> Boolean

public fun <T : Token> openingBracketCharEventHandler(
public fun UniversalKeyboardEventHandler.asKeyboardEventHandler(): KeyboardEventHandler = {
if (it is UniversalKeyboardEvent) invoke(it) else null
}

public fun UniversalKeyboardEventFilter.asKeyboardEventFilter(): KeyboardEventFilter = {
it is UniversalKeyboardEvent && invoke(it)
}

public fun <T : Token> openingBracketEventHandler(
textFieldState: BasicSourceCodeTextFieldState<T>,
openingChar: Char, openingBracket: String, closingBracket: String,
addNewLinesForSelection: (CharEvent.Insert) -> Boolean = { false },
addNewLinesForSelection: (UniversalKeyboardEvent.Insert) -> Boolean = { false },
indent: String? = " ".repeat(4),
): CharEventHandler = f@{ keyEvent ->
if (keyEvent !is CharEvent.Insert || keyEvent.char != openingChar) return@f null
): KeyboardEventHandler = f@{ keyEvent ->
if (keyEvent !is UniversalKeyboardEvent.Insert || keyEvent.char != openingChar) return@f null
val oldSelection = textFieldState.selection
val minLine = textFieldState.sourceCodePositions[oldSelection.min].line
val maxLine = textFieldState.sourceCodePositions[oldSelection.max].line
Expand Down Expand Up @@ -94,12 +104,12 @@ public fun <T : Token> openingBracketCharEventHandler(
TextFieldValue(newString, newSelection, newComposition)
}

public inline fun <T : Token, reified Bracket : ScopeChangingToken> closingBracketCharEventHandler(
public inline fun <T : Token, reified Bracket : ScopeChangingToken> closingBracketEventHandler(
textFieldState: BasicSourceCodeTextFieldState<T>,
matchedBrackets: Map<Bracket, Bracket>,
openingBracket: String, closingChar: Char, closingBracket: String,
): CharEventHandler = f@{ keyEvent ->
if (keyEvent !is CharEvent.Insert || keyEvent.char != closingChar) return@f null
): KeyboardEventHandler = f@{ keyEvent ->
if (keyEvent !is UniversalKeyboardEvent.Insert || keyEvent.char != closingChar) return@f null

val position = textFieldState.sourceCodePositions[textFieldState.selection.min]
val finishLine = position.line
Expand Down Expand Up @@ -152,9 +162,9 @@ public inline fun <T : Token, reified Bracket : ScopeChangingToken> closingBrack
public fun <T : Token> reusingCharsEventHandler(
textFieldState: BasicSourceCodeTextFieldState<T>,
chars: String,
): CharEventHandler = f@{ keyEvent ->
): KeyboardEventHandler = f@{ keyEvent ->
if (
keyEvent !is CharEvent.Insert || keyEvent.char !in chars || !textFieldState.selection.collapsed ||
keyEvent !is UniversalKeyboardEvent.Insert || keyEvent.char !in chars || !textFieldState.selection.collapsed ||
textFieldState.selection.start == textFieldState.text.length ||
textFieldState.text[textFieldState.selection.start] != keyEvent.char
) return@f null
Expand All @@ -165,12 +175,12 @@ public fun <T : Token> reusingCharsEventHandler(
)
}

public inline fun <reified T : Token, reified Bracket : ScopeChangingToken> newLineCharEventHandler(
public inline fun <reified T : Token, reified Bracket : ScopeChangingToken> newLineEventHandler(
textFieldState: BasicSourceCodeTextFieldState<T>,
matchedBrackets: Map<Bracket, Bracket>,
indent: String = " ".repeat(4),
): CharEventHandler = f@{ keyEvent ->
if (keyEvent !is CharEvent.Insert || keyEvent.char != '\n') return@f null
): KeyboardEventHandler = f@{ keyEvent ->
if (keyEvent !is UniversalKeyboardEvent.Insert || keyEvent.char != '\n') return@f null
val currentLine = textFieldState.sourceCodePositions[textFieldState.selection.min].line
val newIndents = textFieldState.tokens.filter {
if (it !is Bracket) return@filter false
Expand Down Expand Up @@ -231,12 +241,12 @@ public inline fun <reified T : Token, reified Bracket : ScopeChangingToken> newL
)
}

public fun <T : Token> removeIndentBackspaceCharEventHandler(
public fun <T : Token> removeIndentBackspaceEventHandler(
textFieldState: BasicSourceCodeTextFieldState<T>,
indent: String = " ".repeat(4),
): CharEventHandler = f@{ keyEvent ->
): KeyboardEventHandler = f@{ keyEvent ->
val offset = textFieldState.selection.start
if (keyEvent !is CharEvent.Backspace || !textFieldState.selection.collapsed || offset < indent.length) return@f null
if (keyEvent !is UniversalKeyboardEvent.Backspace || !textFieldState.selection.collapsed || offset < indent.length) return@f null
val position = textFieldState.sourceCodePositions[offset]
val line = position.line
val lineOffset = textFieldState.lineOffsets[line]
Expand All @@ -257,14 +267,14 @@ public fun <T : Token> removeIndentBackspaceCharEventHandler(
)
}

public fun <T : Token> removeEmptyBracesBackspaceCharEventHandler(
public fun <T : Token> removeEmptyBracesBackspaceEventHandler(
textFieldState: BasicSourceCodeTextFieldState<T>,
openingBracket: String,
closingBracket: String,
): CharEventHandler = f@{ keyEvent ->
): KeyboardEventHandler = f@{ keyEvent ->
val offset = textFieldState.selection.start
if (
keyEvent !is CharEvent.Backspace || !textFieldState.selection.collapsed || offset < openingBracket.length ||
keyEvent !is UniversalKeyboardEvent.Backspace || !textFieldState.selection.collapsed || offset < openingBracket.length ||
offset + closingBracket.length > textFieldState.text.length
) return@f null
for ((i, c) in openingBracket.withIndex()) {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ development=true

#Publication
group=com.zhelenskiy
version=0.0.12
version=0.0.13

0 comments on commit 41a2475

Please sign in to comment.