Categories
Compose for Desktop Kotlin Kotlin Multiplatform

Create a code editor with Compose for Desktop

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 is meant for single line of text

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.
BasicTextField does not respect code indentation

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.

CodeViewer does not enable editing of code

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.

Compose Desktop code editor with a Swing code editor component

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.

Leave a Reply