Compose desktop is an upcoming cross-platform framework that promises the building of apps from a single codebase to every desktop and web platform. Since Kotlin Multiplatform is gaining traction, a cross-platform UI framework on top of it seems like a logical step forward.
There are tutorials and examples about creating simple widgets and layouts, but how about creating a more complicated code editor?
Native components
In Compose, the native text editor widget is the TextField. Its UI is derived from the mobile world, and thus looks more like a single line field, than a multi line code editor.
TextField’s sister class, BasicTextField, provides an undecorated multiline editor that could be a good solution for our code editor view.
val textFieldValueState = remember {
mutableStateOf(
TextFieldValue(text = "init\n init")
)
}
BasicTextField(
modifier = Modifier.fillMaxSize(),
value = textFieldValueState.value,
onValueChange = { tfv ->
textFieldValueState.value = tfv
},
)
However, this widget is meant for writing text, not code. For this reason, many code editor features are missing:
- There is no auto appending tabs for new lines.
- Pressing tab only adds 1 space character.
- The caret does not jump to the correct tab position when navigating.
The tab to 4 spaces logic could be written using the onValueChange callback:
onKeyEvent {
when {
(it.key == Key.Tab) -> {
// Add 4 spaces after pressing tab
val newText = textFieldValueState.value.text + " "
val length = newText.length
textFieldValueState.value =
TextFieldValue(
text = newText,
selection = TextRange(length, length)
)
true
}
else -> false
}
}
But there are many more edge cases to consider. For example: line numbers or syntax highlighting. The developer would certainly prefer a pre-made out of the box solution.
OSS projects
There are 2 examples from the official Compose repo that could be considered as a code editor.
Notepad
Notepad contains a large text editor for taking notes. It uses BasicTextField as the text editor, which was unfortunately discarded as possible solution in the previous paragraph.
CodeViewer
CodeViewer is a full blown code viewer. It has some syntax highlighting and correct tab behaviour. Besides being a great code viewing app, there unfortunately is no code editing option. This is because the text is a static Text
, and cannot be modified.
Since we are trying to edit code, there has to be another solution.
Embedding the editor in a WebView
There are code editors that can be used in a Web browser. For React, one popular and minimalistic option is satya164/react-simple-code-editor. In Compose Android this could be added via the AndroidView.
However, in Compose Desktop, there doesn’t seem to be a way to embed WebViews. The user would either need to create a react-native or compose for web app and include the code editor in a web page there.
Swing plugin
Compose documentation describes Swing Integration. Therefore it should be possible to embed an existing Swing view inside a Compose app.
bobbylight/RSyntaxTextArea
RSyntaxTextArea
is a customisable, syntax highlighting text component for Java Swing applications. It has code indentation, folding and highlighting features. In addition, user can add-on code completion and syntax highlighting libraries.
Adding a Swing component
Since RSyntaxTextArea
is written in Swing, the developer needs to write some extra code to include it in Compose Desktop. Swing view can be added inside the factory
parameter in aSwingPanel
component.
SwingPanel(
modifier = Modifier.size(270.dp, 90.dp),
factory = {
JPanel().apply {
// Swing view
After that, the add()
function is used to include the RSyntaxTextArea
:
var rtCodeEditor: RTextScrollPane? = null
@Composable
fun CodeEditor(code: MutableState<String>, modifier: Modifier = Modifier) {
if (rtCodeEditor == null) {
val textArea = RSyntaxTextArea(20, 60);
val sp = RTextScrollPane(textArea)
sp.textArea.text = code.value
// listen for code changes and update Compose state
sp.textArea.addCaretListener { code.value = sp.textArea.text }
rtCodeEditor = sp
}
...
factory = {
JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
add(rtCodeEditor)
}
rtCodeEditor
is created only once, because otherwise a new code editor would be created on every Compose cycle. Since we change the app state with addCaretListener { code.value = sp.textArea.text }
, it would be on every character change in the code editor.
For more information, read about state in Compose.
Creating a composable function
CodeEditor
can be embedded in a composable window
@Composable
fun CodeEditorWindow(state: AppState) {
Window(onCloseRequest = {}, title = "Noium") {
Column {
CodeEditor(state.editorText, modifier = Modifier.fillMaxSize().padding(10.dp))
}
}
}
and then added as a window to a Compose app
fun main() = application {
val state = rememberAppState()
CodeEditorWindow(state)
}
Result
The result is a fully fledged code editor in a Compose Desktop app. Thanks to RSyntaxTextArea and Swing integration, the developer needs to write only a minimal amount of code. And the result in MacOS feels as performant as any other IDE.
The code is available in Github
Conclusion
There has never been a better time to create a cross-platform code editor. With only 100 lines of code, the backbone of the editor is ready and shippable with ./gradlew packageDmg
command. Furthermore the code can be compiled to any of the desktop platforms.
It is up to the developer to find a use case for this or any other possibility the Compose Desktop offers.
How to create a code editor with Compose for Desktop:https://t.co/t6hmmU4mgF
— Tõnis Tiganik (@tonisives) August 31, 2021
Hint: embed a Swing view #kotlin #JetpackCompose