diff --git a/examples/codeviewer/.gitignore b/examples/codeviewer/.gitignore
new file mode 100644
index 00000000..ba8435b9
--- /dev/null
+++ b/examples/codeviewer/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+build/
+/captures
+.externalNativeBuild
+.cxx
\ No newline at end of file
diff --git a/examples/codeviewer/.run/desktop.run.xml b/examples/codeviewer/.run/desktop.run.xml
new file mode 100644
index 00000000..d9335c1b
--- /dev/null
+++ b/examples/codeviewer/.run/desktop.run.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
\ No newline at end of file
diff --git a/examples/codeviewer/README.md b/examples/codeviewer/README.md
new file mode 100644
index 00000000..364bc6e2
--- /dev/null
+++ b/examples/codeviewer/README.md
@@ -0,0 +1,7 @@
+MPP Code Viewer example for desktop/android written in Multiplatform Compose library.
+
+To run desktop application execute in a terminal:
+`./gradlew desktop:run`
+
+To install android application on device/emulator:
+'./gradlew installDebug'
\ No newline at end of file
diff --git a/examples/codeviewer/android/build.gradle.kts b/examples/codeviewer/android/build.gradle.kts
new file mode 100644
index 00000000..20040c60
--- /dev/null
+++ b/examples/codeviewer/android/build.gradle.kts
@@ -0,0 +1,25 @@
+plugins {
+ id("com.android.application")
+ kotlin("android")
+ id("org.jetbrains.compose")
+}
+
+android {
+ compileSdkVersion(30)
+
+ defaultConfig {
+ minSdkVersion(26)
+ targetSdkVersion(30)
+ versionCode = 1
+ versionName = "1.0"
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation(project(":common"))
+}
\ No newline at end of file
diff --git a/examples/codeviewer/android/src/main/AndroidManifest.xml b/examples/codeviewer/android/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..6e3e6786
--- /dev/null
+++ b/examples/codeviewer/android/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/codeviewer/android/src/main/assets/data/EditorView.kt b/examples/codeviewer/android/src/main/assets/data/EditorView.kt
new file mode 100644
index 00000000..cac51452
--- /dev/null
+++ b/examples/codeviewer/android/src/main/assets/data/EditorView.kt
@@ -0,0 +1,187 @@
+package org.jetbrains.codeviewer.ui.editor
+
+import androidx.compose.foundation.AmbientContentColor
+import androidx.compose.foundation.Text
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawOpacity
+import androidx.compose.ui.platform.DensityAmbient
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import org.jetbrains.codeviewer.platform.SelectionContainer
+import org.jetbrains.codeviewer.platform.VerticalScrollbar
+import org.jetbrains.codeviewer.platform.WithoutSelection
+import org.jetbrains.codeviewer.ui.common.AppTheme
+import org.jetbrains.codeviewer.ui.common.Fonts
+import org.jetbrains.codeviewer.ui.common.Settings
+import org.jetbrains.codeviewer.util.LazyColumnFor
+import org.jetbrains.codeviewer.util.loadable
+import org.jetbrains.codeviewer.util.loadableScoped
+import org.jetbrains.codeviewer.util.withoutWidthConstraints
+import kotlin.text.Regex.Companion.fromLiteral
+
+@Composable
+fun EditorView(model: Editor, settings: Settings) = key(model) {
+ with (DensityAmbient.current) {
+ SelectionContainer {
+ Surface(
+ Modifier.fillMaxSize(),
+ color = AppTheme.colors.backgroundDark,
+ ) {
+ val lines by loadableScoped(model.lines)
+
+ if (lines != null) {
+ Box {
+ Lines(lines!!, settings)
+ Box(
+ Modifier
+ .offset(
+ x = settings.fontSize.toDp() * 0.5f * settings.maxLineSymbols
+ )
+ .width(1.dp)
+ .fillMaxHeight()
+ .background(AppTheme.colors.backgroundLight)
+ )
+ }
+ } else {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .size(36.dp)
+ .padding(4.dp)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun Lines(lines: Editor.Lines, settings: Settings) = with(DensityAmbient.current) {
+ val maxNum = remember(lines.lineNumberDigitCount) {
+ (1..lines.lineNumberDigitCount).joinToString(separator = "") { "9" }
+ }
+
+ Box(Modifier.fillMaxSize()) {
+ val scrollState = rememberLazyListState()
+ val lineHeight = settings.fontSize.toDp() * 1.6f
+
+ LazyColumnFor(
+ lines.size,
+ modifier = Modifier.fillMaxSize(),
+ state = scrollState,
+ itemContent = { index ->
+ val line: Editor.Line? by loadable { lines.get(index) }
+ Box(Modifier.height(lineHeight)) {
+ if (line != null) {
+ Line(Modifier.align(Alignment.CenterStart), maxNum, line!!, settings)
+ }
+ }
+ }
+ )
+
+ VerticalScrollbar(
+ Modifier.align(Alignment.CenterEnd),
+ scrollState,
+ lines.size,
+ lineHeight
+ )
+ }
+}
+
+// Поддержка русского языка
+// دعم اللغة العربية
+// 中文支持
+@Composable
+private fun Line(modifier: Modifier, maxNum: String, line: Editor.Line, settings: Settings) {
+ Row(modifier = modifier) {
+ WithoutSelection {
+ Box {
+ LineNumber(maxNum, Modifier.drawOpacity(0f), settings)
+ LineNumber(line.number.toString(), Modifier.align(Alignment.CenterEnd), settings)
+ }
+ }
+ LineContent(
+ line.content,
+ modifier = Modifier
+ .weight(1f)
+ .withoutWidthConstraints()
+ .padding(start = 28.dp, end = 12.dp),
+ settings = settings
+ )
+ }
+}
+
+@Composable
+private fun LineNumber(number: String, modifier: Modifier, settings: Settings) = Text(
+ text = number,
+ fontSize = settings.fontSize,
+ fontFamily = Fonts.jetbrainsMono(),
+ color = AmbientContentColor.current.copy(alpha = 0.30f),
+ modifier = modifier.padding(start = 12.dp)
+)
+
+@Composable
+private fun LineContent(content: Editor.Content, modifier: Modifier, settings: Settings) = Text(
+ text = if (content.isCode) {
+ codeString(content.value.value)
+ } else {
+ AnnotatedString(content.value.value)
+ },
+ fontSize = settings.fontSize,
+ fontFamily = Fonts.jetbrainsMono(),
+ modifier = modifier,
+ softWrap = false
+)
+
+private fun codeString(str: String) = annotatedString {
+ withStyle(AppTheme.code.simple) {
+ append(str.replace("\t", " "))
+ addStyle(AppTheme.code.punctuation, ":")
+ addStyle(AppTheme.code.punctuation, "=")
+ addStyle(AppTheme.code.punctuation, "\"")
+ addStyle(AppTheme.code.punctuation, "[")
+ addStyle(AppTheme.code.punctuation, "]")
+ addStyle(AppTheme.code.punctuation, "{")
+ addStyle(AppTheme.code.punctuation, "}")
+ addStyle(AppTheme.code.punctuation, "(")
+ addStyle(AppTheme.code.punctuation, ")")
+ addStyle(AppTheme.code.punctuation, ",")
+ addStyle(AppTheme.code.keyword, "fun ")
+ addStyle(AppTheme.code.keyword, "val ")
+ addStyle(AppTheme.code.keyword, "var ")
+ addStyle(AppTheme.code.keyword, "private ")
+ addStyle(AppTheme.code.keyword, "internal ")
+ addStyle(AppTheme.code.keyword, "for ")
+ addStyle(AppTheme.code.keyword, "expect ")
+ addStyle(AppTheme.code.keyword, "actual ")
+ addStyle(AppTheme.code.keyword, "import ")
+ addStyle(AppTheme.code.keyword, "package ")
+ addStyle(AppTheme.code.value, "true")
+ addStyle(AppTheme.code.value, "false")
+ addStyle(AppTheme.code.value, Regex("[0-9]*"))
+ addStyle(AppTheme.code.annotation, Regex("^@[a-zA-Z_]*"))
+ addStyle(AppTheme.code.comment, Regex("^\\s*//.*"))
+ }
+}
+
+private fun AnnotatedString.Builder.addStyle(style: SpanStyle, regexp: String) {
+ addStyle(style, fromLiteral(regexp))
+}
+
+private fun AnnotatedString.Builder.addStyle(style: SpanStyle, regexp: Regex) {
+ for (result in regexp.findAll(toString())) {
+ addStyle(style, result.range.first, result.range.last + 1)
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/android/src/main/java/org/jetbrains/codeviewer/MainActivity.kt b/examples/codeviewer/android/src/main/java/org/jetbrains/codeviewer/MainActivity.kt
new file mode 100644
index 00000000..eb7a4705
--- /dev/null
+++ b/examples/codeviewer/android/src/main/java/org/jetbrains/codeviewer/MainActivity.kt
@@ -0,0 +1,33 @@
+package org.jetbrains.codeviewer
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.ui.platform.setContent
+import org.jetbrains.codeviewer.platform._HomeFolder
+import org.jetbrains.codeviewer.ui.MainView
+import java.io.File
+import java.io.FileOutputStream
+
+
+class MainActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ copyAssets()
+ _HomeFolder = filesDir
+
+ setContent {
+ MainView()
+ }
+ }
+
+ private fun copyAssets() {
+ for (filename in assets.list("data")!!) {
+ assets.open("data/$filename").use { assetStream ->
+ val file = File(filesDir, filename)
+ FileOutputStream(file).use { fileStream ->
+ assetStream.copyTo(fileStream)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/android/src/main/res/drawable/ic_launcher_foreground.xml b/examples/codeviewer/android/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..cecdd7d0
--- /dev/null
+++ b/examples/codeviewer/android/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/examples/codeviewer/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/codeviewer/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..7353dbd1
--- /dev/null
+++ b/examples/codeviewer/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/codeviewer/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/codeviewer/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..7353dbd1
--- /dev/null
+++ b/examples/codeviewer/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/codeviewer/android/src/main/res/values/ic_launcher_background.xml b/examples/codeviewer/android/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 00000000..5950cfc1
--- /dev/null
+++ b/examples/codeviewer/android/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #3C3F41
+
\ No newline at end of file
diff --git a/examples/codeviewer/android/src/main/res/values/strings.xml b/examples/codeviewer/android/src/main/res/values/strings.xml
new file mode 100644
index 00000000..19c9645e
--- /dev/null
+++ b/examples/codeviewer/android/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Code Viewer
+
\ No newline at end of file
diff --git a/examples/codeviewer/build.gradle.kts b/examples/codeviewer/build.gradle.kts
new file mode 100644
index 00000000..386db2e2
--- /dev/null
+++ b/examples/codeviewer/build.gradle.kts
@@ -0,0 +1,23 @@
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
+ }
+
+ dependencies {
+ // TODO/migrateToMaster 0.1.0-dev104 is built from "unmerged" branch,
+ // replace it by version from androidx-master-dev when scrollbars will be merged
+ classpath("org.jetbrains.compose:compose-gradle-plugin:0.1.0-dev104")
+ classpath("com.android.tools.build:gradle:4.0.1")
+ classpath(kotlin("gradle-plugin", version = "1.4.0"))
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/build.gradle.kts b/examples/codeviewer/common/build.gradle.kts
new file mode 100644
index 00000000..c831fa0d
--- /dev/null
+++ b/examples/codeviewer/common/build.gradle.kts
@@ -0,0 +1,66 @@
+import org.jetbrains.compose.compose
+
+plugins {
+ id("com.android.library")
+ kotlin("multiplatform")
+ id("org.jetbrains.compose")
+}
+
+kotlin {
+ android()
+ jvm("desktop")
+
+ sourceSets {
+ named("commonMain") {
+ dependencies {
+ api(compose.runtime)
+ api(compose.foundation)
+ api(compose.material)
+ api(compose.materialIconsExtended)
+ }
+ }
+ named("androidMain") {
+ kotlin.srcDirs("src/jvmMain/kotlin")
+ dependencies {
+ api("androidx.appcompat:appcompat:1.1.0")
+ api("androidx.core:core-ktx:1.3.1")
+ }
+ }
+ named("desktopMain") {
+ kotlin.srcDirs("src/jvmMain/kotlin")
+ resources.srcDirs("src/commonMain/resources")
+ dependencies {
+ api(compose.desktop.common)
+ }
+ }
+ }
+}
+
+project.extensions.findByType()!!.apply {
+ sourceSets.findByName("main")?.apply {
+ res.srcDirs("src/commonMain/resources")
+ }
+}
+
+android {
+ compileSdkVersion(30)
+
+ defaultConfig {
+ minSdkVersion(21)
+ targetSdkVersion(30)
+ versionCode = 1
+ versionName = "1.0"
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+
+ sourceSets {
+ named("main") {
+ manifest.srcFile("src/androidMain/AndroidManifest.xml")
+ res.srcDirs("src/androidMain/res")
+ }
+ }
+}
diff --git a/examples/codeviewer/common/src/androidMain/AndroidManifest.xml b/examples/codeviewer/common/src/androidMain/AndroidManifest.xml
new file mode 100644
index 00000000..59fb71fb
--- /dev/null
+++ b/examples/codeviewer/common/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/File.kt b/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/File.kt
new file mode 100644
index 00000000..497976a7
--- /dev/null
+++ b/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/File.kt
@@ -0,0 +1,4 @@
+package org.jetbrains.codeviewer.platform
+
+lateinit var _HomeFolder: java.io.File
+actual val HomeFolder: File get() = _HomeFolder.toProjectFile()
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt b/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt
new file mode 100644
index 00000000..d35c3002
--- /dev/null
+++ b/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt
@@ -0,0 +1,12 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+
+actual fun Modifier.pointerMoveFilter(
+ onEnter: () -> Boolean,
+ onExit: () -> Boolean,
+ onMove: (Offset) -> Boolean
+): Modifier = this
+
+actual fun Modifier.cursorForHorizontalResize() = this
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt b/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt
new file mode 100644
index 00000000..bcf0741e
--- /dev/null
+++ b/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt
@@ -0,0 +1,31 @@
+package org.jetbrains.codeviewer.platform
+
+import android.graphics.BitmapFactory
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.ImageAsset
+import androidx.compose.ui.graphics.asImageAsset
+import androidx.compose.ui.platform.ContextAmbient
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import java.io.InputStream
+import java.net.URL
+
+@Composable
+actual fun imageResource(res: String): ImageAsset {
+ val context = ContextAmbient.current
+ val id = context.resources.getIdentifier(res, "drawable", context.packageName)
+ return androidx.compose.ui.res.imageResource(id)
+}
+
+actual suspend fun imageFromUrl(url: String): ImageAsset {
+ val bytes = URL(url).openStream().buffered().use(InputStream::readBytes)
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.size).asImageAsset()
+}
+
+@Composable
+actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font {
+ val context = ContextAmbient.current
+ val id = context.resources.getIdentifier(res, "font", context.packageName)
+ return androidx.compose.ui.text.font.font(id, weight, style)
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt b/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt
new file mode 100644
index 00000000..95be105f
--- /dev/null
+++ b/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt
@@ -0,0 +1,21 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+
+@Composable
+actual fun VerticalScrollbar(
+ modifier: Modifier,
+ scrollState: ScrollState
+) = Unit
+
+@Composable
+actual fun VerticalScrollbar(
+ modifier: Modifier,
+ scrollState: LazyListState,
+ itemCount: Int,
+ averageItemSize: Dp
+) = Unit
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt b/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt
new file mode 100644
index 00000000..c171e5bc
--- /dev/null
+++ b/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt
@@ -0,0 +1,26 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.selection.Selection
+import androidx.compose.ui.selection.SelectionContainer
+
+@Composable
+actual fun SelectionContainer(children: @Composable () -> Unit) {
+ val selection = remember { mutableStateOf(null) }
+ SelectionContainer(
+ selection = selection.value,
+ onSelectionChange = { selection.value = it },
+ children = children
+ )
+}
+
+@Composable
+actual fun WithoutSelection(children: @Composable () -> Unit) {
+ SelectionContainer(
+ selection = null,
+ onSelectionChange = {},
+ children = children
+ )
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt b/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt
new file mode 100644
index 00000000..7565fa1a
--- /dev/null
+++ b/examples/codeviewer/common/src/androidMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt
@@ -0,0 +1,6 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.runtime.Composable
+
+@Composable
+actual fun PlatformTheme(content: @Composable () -> Unit) = content()
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/File.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/File.kt
new file mode 100644
index 00000000..970291bf
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/File.kt
@@ -0,0 +1,15 @@
+package org.jetbrains.codeviewer.platform
+
+import kotlinx.coroutines.CoroutineScope
+import org.jetbrains.codeviewer.util.TextLines
+
+expect val HomeFolder: File
+
+interface File {
+ val name: String
+ val isDirectory: Boolean
+ val children: List
+ val hasChildren: Boolean
+
+ suspend fun readLines(backgroundScope: CoroutineScope): TextLines
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt
new file mode 100644
index 00000000..4ce937e2
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt
@@ -0,0 +1,12 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+
+expect fun Modifier.pointerMoveFilter(
+ onEnter: () -> Boolean = { true },
+ onExit: () -> Boolean = { true },
+ onMove: (Offset) -> Boolean = { true }
+): Modifier
+
+expect fun Modifier.cursorForHorizontalResize(): Modifier
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt
new file mode 100644
index 00000000..0f72f42a
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt
@@ -0,0 +1,15 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.ImageAsset
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+
+@Composable
+expect fun imageResource(res: String): ImageAsset
+
+expect suspend fun imageFromUrl(url: String): ImageAsset
+
+@Composable
+expect fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt
new file mode 100644
index 00000000..e1ea979b
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt
@@ -0,0 +1,21 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+
+@Composable
+expect fun VerticalScrollbar(
+ modifier: Modifier,
+ scrollState: ScrollState
+)
+
+@Composable
+expect fun VerticalScrollbar(
+ modifier: Modifier,
+ scrollState: LazyListState,
+ itemCount: Int,
+ averageItemSize: Dp
+)
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt
new file mode 100644
index 00000000..f071f47d
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt
@@ -0,0 +1,9 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.runtime.Composable
+
+@Composable
+expect fun SelectionContainer(children: @Composable () -> Unit)
+
+@Composable
+expect fun WithoutSelection(children: @Composable () -> Unit)
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt
new file mode 100644
index 00000000..3c94f7e4
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt
@@ -0,0 +1,6 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.runtime.Composable
+
+@Composable
+expect fun PlatformTheme(content: @Composable () -> Unit)
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/CodeViewer.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/CodeViewer.kt
new file mode 100644
index 00000000..9d65e5b2
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/CodeViewer.kt
@@ -0,0 +1,11 @@
+package org.jetbrains.codeviewer.ui
+
+import org.jetbrains.codeviewer.ui.common.Settings
+import org.jetbrains.codeviewer.ui.editor.Editors
+import org.jetbrains.codeviewer.ui.filetree.FileTree
+
+class CodeViewer(
+ val editors: Editors,
+ val fileTree: FileTree,
+ val settings: Settings
+)
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/CodeViewerView.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/CodeViewerView.kt
new file mode 100644
index 00000000..0b694cea
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/CodeViewerView.kt
@@ -0,0 +1,105 @@
+package org.jetbrains.codeviewer.ui
+
+import androidx.compose.animation.animate
+import androidx.compose.animation.core.Spring.StiffnessLow
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.foundation.AmbientContentColor
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.Icon
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.ArrowForward
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.drawLayer
+import androidx.compose.ui.unit.dp
+import org.jetbrains.codeviewer.ui.editor.EditorEmptyView
+import org.jetbrains.codeviewer.ui.editor.EditorTabsView
+import org.jetbrains.codeviewer.ui.editor.EditorView
+import org.jetbrains.codeviewer.ui.filetree.FileTreeView
+import org.jetbrains.codeviewer.ui.filetree.FileTreeViewTabView
+import org.jetbrains.codeviewer.ui.statusbar.StatusBar
+import org.jetbrains.codeviewer.util.SplitterState
+import org.jetbrains.codeviewer.util.VerticalSplitable
+
+@Composable
+fun CodeViewerView(model: CodeViewer) {
+ val panelState = remember { PanelState() }
+
+ val animatedSize = if (panelState.splitter.isResizing) {
+ if (panelState.isExpanded) panelState.expandedSize else panelState.collapsedSize
+ } else {
+ animate(
+ if (panelState.isExpanded) panelState.expandedSize else panelState.collapsedSize,
+ SpringSpec(stiffness = StiffnessLow)
+ )
+ }
+
+ VerticalSplitable(
+ Modifier.fillMaxSize(),
+ panelState.splitter,
+ onResize = {
+ panelState.expandedSize =
+ (panelState.expandedSize + it).coerceAtLeast(panelState.expandedSizeMin)
+ }
+ ) {
+ ResizablePanel(Modifier.width(animatedSize).fillMaxHeight(), panelState) {
+ Column {
+ FileTreeViewTabView()
+ FileTreeView(model.fileTree)
+ }
+ }
+
+ Box {
+ if (model.editors.active != null) {
+ Column(Modifier.fillMaxSize()) {
+ EditorTabsView(model.editors)
+ Box(Modifier.weight(1f)) {
+ EditorView(model.editors.active!!, model.settings)
+ }
+ StatusBar(model.settings)
+ }
+ } else {
+ EditorEmptyView()
+ }
+ }
+ }
+}
+
+private class PanelState {
+ val collapsedSize = 24.dp
+ var expandedSize by mutableStateOf(300.dp)
+ val expandedSizeMin = 90.dp
+ var isExpanded by mutableStateOf(true)
+ val splitter = SplitterState()
+}
+
+@Composable
+private fun ResizablePanel(
+ modifier: Modifier,
+ state: PanelState,
+ content: @Composable () -> Unit,
+) {
+ val alpha = animate(if (state.isExpanded) 1f else 0f, SpringSpec(stiffness = StiffnessLow))
+
+ Box(modifier) {
+ Box(Modifier.fillMaxSize().drawLayer(alpha = alpha)) {
+ content()
+ }
+
+ Icon(
+ if (state.isExpanded) Icons.Default.ArrowBack else Icons.Default.ArrowForward,
+ tint = AmbientContentColor.current,
+ modifier = Modifier
+ .padding(top = 4.dp)
+ .width(24.dp)
+ .clickable {
+ state.isExpanded = !state.isExpanded
+ }
+ .padding(4.dp)
+ .align(Alignment.TopEnd)
+ )
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/MainView.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/MainView.kt
new file mode 100644
index 00000000..5b257fd2
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/MainView.kt
@@ -0,0 +1,38 @@
+package org.jetbrains.codeviewer.ui
+
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import org.jetbrains.codeviewer.platform.HomeFolder
+import org.jetbrains.codeviewer.platform.PlatformTheme
+import org.jetbrains.codeviewer.platform.WithoutSelection
+import org.jetbrains.codeviewer.ui.common.AppTheme
+import org.jetbrains.codeviewer.ui.common.Settings
+import org.jetbrains.codeviewer.ui.editor.Editors
+import org.jetbrains.codeviewer.ui.filetree.FileTree
+
+@Composable
+fun MainView() {
+ val codeViewer = remember {
+ val editors = Editors()
+
+ CodeViewer(
+ editors = editors,
+ fileTree = FileTree(HomeFolder, editors),
+ settings = Settings()
+ )
+ }
+
+ WithoutSelection {
+ MaterialTheme(
+ colors = AppTheme.colors.material
+ ) {
+ PlatformTheme {
+ Surface {
+ CodeViewerView(codeViewer)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Fonts.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Fonts.kt
new file mode 100644
index 00000000..026efa2f
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Fonts.kt
@@ -0,0 +1,64 @@
+package org.jetbrains.codeviewer.ui.common
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.font.fontFamily
+import org.jetbrains.codeviewer.platform.font
+
+object Fonts {
+ @Composable
+ fun jetbrainsMono() = fontFamily(
+ font(
+ "JetBrains Mono",
+ "jetbrainsmono_regular",
+ FontWeight.Normal,
+ FontStyle.Normal
+ ),
+ font(
+ "JetBrains Mono",
+ "jetbrainsmono_italic",
+ FontWeight.Normal,
+ FontStyle.Italic
+ ),
+
+ font(
+ "JetBrains Mono",
+ "jetbrainsmono_bold",
+ FontWeight.Bold,
+ FontStyle.Normal
+ ),
+ font(
+ "JetBrains Mono",
+ "jetbrainsmono_bold_italic",
+ FontWeight.Bold,
+ FontStyle.Italic
+ ),
+
+ font(
+ "JetBrains Mono",
+ "jetbrainsmono_extrabold",
+ FontWeight.ExtraBold,
+ FontStyle.Normal
+ ),
+ font(
+ "JetBrains Mono",
+ "jetbrainsmono_extrabold_italic",
+ FontWeight.ExtraBold,
+ FontStyle.Italic
+ ),
+
+ font(
+ "JetBrains Mono",
+ "jetbrainsmono_medium",
+ FontWeight.Medium,
+ FontStyle.Normal
+ ),
+ font(
+ "JetBrains Mono",
+ "jetbrainsmono_medium_italic",
+ FontWeight.Medium,
+ FontStyle.Italic
+ )
+ )
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Settings.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Settings.kt
new file mode 100644
index 00000000..7b1d27e5
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Settings.kt
@@ -0,0 +1,11 @@
+package org.jetbrains.codeviewer.ui.common
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.sp
+
+class Settings {
+ var fontSize by mutableStateOf(13.sp)
+ val maxLineSymbols = 120
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Theme.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Theme.kt
new file mode 100644
index 00000000..b849ea4a
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/common/Theme.kt
@@ -0,0 +1,32 @@
+package org.jetbrains.codeviewer.ui.common
+
+import androidx.compose.material.darkColors
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.SpanStyle
+
+object AppTheme {
+ val colors: Colors = Colors()
+
+ val code: Code = Code()
+
+ class Colors(
+ val backgroundDark: Color = Color(0xFF2B2B2B),
+ val backgroundMedium: Color = Color(0xFF3C3F41),
+ val backgroundLight: Color = Color(0xFF4E5254),
+
+ val material: androidx.compose.material.Colors = darkColors(
+ background = backgroundDark,
+ surface = backgroundMedium,
+ primary = Color.White
+ ),
+ )
+
+ class Code(
+ val simple: SpanStyle = SpanStyle(Color(0xFFA9B7C6)),
+ val value: SpanStyle = SpanStyle(Color(0xFF6897BB)),
+ val keyword: SpanStyle = SpanStyle(Color(0xFFCC7832)),
+ val punctuation: SpanStyle = SpanStyle(Color(0xFFA1C17E)),
+ val annotation: SpanStyle = SpanStyle(Color(0xFFBBB529)),
+ val comment: SpanStyle = SpanStyle(Color(0xFF808080))
+ )
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt
new file mode 100644
index 00000000..79897fb2
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt
@@ -0,0 +1,58 @@
+package org.jetbrains.codeviewer.ui.editor
+
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import kotlinx.coroutines.CoroutineScope
+import org.jetbrains.codeviewer.platform.File
+import org.jetbrains.codeviewer.util.SingleSelection
+import org.jetbrains.codeviewer.util.afterSet
+
+class Editor(
+ val fileName: String,
+ val lines: suspend (backgroundScope: CoroutineScope) -> Lines,
+) {
+ var close: (() -> Unit)? = null
+ lateinit var selection: SingleSelection
+
+ val isActive: Boolean
+ get() = selection.selected === this
+
+ fun activate() {
+ selection.selected = this
+ }
+
+ class Line(val number: Int, val content: Content)
+
+ interface Lines {
+ val lineNumberDigitCount: Int get() = size.toString().length
+ val size: Int
+ suspend fun get(index: Int): Line
+ }
+
+ class Content(val value: State, val isCode: Boolean)
+}
+
+fun Editor(file: File) = Editor(
+ fileName = file.name
+) { backgroundScope ->
+ val textLines = file.readLines(backgroundScope)
+ val indexToEditedText = mutableMapOf()
+ val isCode = file.name.endsWith(".kt", ignoreCase = true)
+
+ suspend fun content(index: Int): Editor.Content {
+ val text = indexToEditedText[index] ?: textLines.get(index)
+ val state = mutableStateOf(text).afterSet {
+ indexToEditedText[index] = it
+ }
+ return Editor.Content(state, isCode)
+ }
+
+ object : Editor.Lines {
+ override val size get() = textLines.size
+
+ override suspend fun get(index: Int) = Editor.Line(
+ number = index + 1,
+ content = content(index)
+ )
+ }
+}
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorEmptyView.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorEmptyView.kt
new file mode 100644
index 00000000..4004aab2
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorEmptyView.kt
@@ -0,0 +1,34 @@
+package org.jetbrains.codeviewer.ui.editor
+
+import androidx.compose.foundation.AmbientContentColor
+import androidx.compose.foundation.Text
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Icon
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Code
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun EditorEmptyView() = Box(Modifier.fillMaxSize()) {
+ Column(Modifier.align(Alignment.Center)) {
+ Icon(
+ Icons.Default.Code.copy(defaultWidth = 48.dp, defaultHeight = 48.dp),
+ tint = AmbientContentColor.current.copy(alpha = 0.60f),
+ modifier = Modifier.align(Alignment.CenterHorizontally)
+ )
+
+ Text(
+ "To view file open it from the file tree",
+ color = AmbientContentColor.current.copy(alpha = 0.60f),
+ fontSize = 20.sp,
+ modifier = Modifier.align(Alignment.CenterHorizontally).padding(16.dp)
+ )
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorTabsView.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorTabsView.kt
new file mode 100644
index 00000000..bc7c1004
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorTabsView.kt
@@ -0,0 +1,72 @@
+package org.jetbrains.codeviewer.ui.editor
+
+import androidx.compose.animation.animate
+import androidx.compose.foundation.AmbientContentColor
+import androidx.compose.foundation.ScrollableRow
+import androidx.compose.foundation.Text
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Icon
+import androidx.compose.material.Surface
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import org.jetbrains.codeviewer.ui.common.AppTheme
+
+@Composable
+fun EditorTabsView(model: Editors) = ScrollableRow {
+ for (editor in model.editors) {
+ EditorTabView(editor)
+ }
+}
+
+@Composable
+fun EditorTabView(model: Editor) = Surface(
+ color = animate(if (model.isActive) {
+ AppTheme.colors.backgroundDark
+ } else {
+ Color.Transparent
+ })
+) {
+ Row(
+ Modifier
+ .clickable {
+ model.activate()
+ }
+ .padding(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ model.fileName,
+ color = AmbientContentColor.current,
+ fontSize = 12.sp,
+ modifier = Modifier.padding(horizontal = 4.dp)
+ )
+
+ val close = model.close
+
+ if (close != null) {
+ Icon(
+ Icons.Default.Close, tint = AmbientContentColor.current, modifier = Modifier
+ .size(24.dp)
+ .padding(4.dp)
+ .clickable {
+ close()
+ })
+ } else {
+ Box(
+ modifier = Modifier
+ .size(24.dp, 24.dp)
+ .padding(4.dp)
+ )
+ }
+ }
+}
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorView.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorView.kt
new file mode 100644
index 00000000..b362697a
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/EditorView.kt
@@ -0,0 +1,191 @@
+package org.jetbrains.codeviewer.ui.editor
+
+import androidx.compose.foundation.AmbientContentColor
+import androidx.compose.foundation.Text
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawOpacity
+import androidx.compose.ui.platform.DensityAmbient
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import org.jetbrains.codeviewer.platform.SelectionContainer
+import org.jetbrains.codeviewer.platform.VerticalScrollbar
+import org.jetbrains.codeviewer.platform.WithoutSelection
+import org.jetbrains.codeviewer.ui.common.AppTheme
+import org.jetbrains.codeviewer.ui.common.Fonts
+import org.jetbrains.codeviewer.ui.common.Settings
+import org.jetbrains.codeviewer.util.LazyColumnFor
+import org.jetbrains.codeviewer.util.loadable
+import org.jetbrains.codeviewer.util.loadableScoped
+import org.jetbrains.codeviewer.util.withoutWidthConstraints
+import kotlin.text.Regex.Companion.fromLiteral
+
+@Composable
+fun EditorView(model: Editor, settings: Settings) = key(model) {
+ with (DensityAmbient.current) {
+ SelectionContainer {
+ Surface(
+ Modifier.fillMaxSize(),
+ color = AppTheme.colors.backgroundDark,
+ ) {
+ val lines by loadableScoped(model.lines)
+
+ if (lines != null) {
+ Box {
+ Lines(lines!!, settings)
+ Box(
+ Modifier
+ .offset(
+ x = settings.fontSize.toDp() * 0.5f * settings.maxLineSymbols
+ )
+ .width(1.dp)
+ .fillMaxHeight()
+ .background(AppTheme.colors.backgroundLight)
+ )
+ }
+ } else {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .size(36.dp)
+ .padding(4.dp)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun Lines(lines: Editor.Lines, settings: Settings) = with(DensityAmbient.current) {
+ val maxNum = remember(lines.lineNumberDigitCount) {
+ (1..lines.lineNumberDigitCount).joinToString(separator = "") { "9" }
+ }
+
+ Box(Modifier.fillMaxSize()) {
+ val scrollState = rememberLazyListState()
+ val lineHeight = settings.fontSize.toDp() * 1.6f
+
+ LazyColumnFor(
+ lines.size,
+ modifier = Modifier.fillMaxSize(),
+ state = scrollState,
+ itemContent = { index ->
+ val line: Editor.Line? by loadable { lines.get(index) }
+ Box(Modifier.height(lineHeight)) {
+ if (line != null) {
+ Line(Modifier.align(Alignment.CenterStart), maxNum, line!!, settings)
+ }
+ }
+ }
+ )
+
+ VerticalScrollbar(
+ Modifier.align(Alignment.CenterEnd),
+ scrollState,
+ lines.size,
+ lineHeight
+ )
+ }
+}
+
+// Поддержка русского языка
+// دعم اللغة العربية
+// 中文支持
+@Composable
+private fun Line(modifier: Modifier, maxNum: String, line: Editor.Line, settings: Settings) {
+ Row(modifier = modifier) {
+ WithoutSelection {
+ Box {
+ LineNumber(maxNum, Modifier.drawOpacity(0f), settings)
+ LineNumber(line.number.toString(), Modifier.align(Alignment.CenterEnd), settings)
+ }
+ }
+ LineContent(
+ line.content,
+ modifier = Modifier
+ .weight(1f)
+ .withoutWidthConstraints()
+ .padding(start = 28.dp, end = 12.dp),
+ settings = settings
+ )
+ }
+}
+
+@Composable
+private fun LineNumber(number: String, modifier: Modifier, settings: Settings) = Text(
+ text = number,
+ fontSize = settings.fontSize,
+ fontFamily = Fonts.jetbrainsMono(),
+ color = AmbientContentColor.current.copy(alpha = 0.30f),
+ modifier = modifier.padding(start = 12.dp)
+)
+
+@Composable
+private fun LineContent(content: Editor.Content, modifier: Modifier, settings: Settings) = Text(
+ text = if (content.isCode) {
+ codeString(content.value.value)
+ } else {
+ annotatedString {
+ withStyle(AppTheme.code.simple) {
+ append(content.value.value)
+ }
+ }
+ },
+ fontSize = settings.fontSize,
+ fontFamily = Fonts.jetbrainsMono(),
+ modifier = modifier,
+ softWrap = false
+)
+
+private fun codeString(str: String) = annotatedString {
+ withStyle(AppTheme.code.simple) {
+ append(str.replace("\t", " "))
+ addStyle(AppTheme.code.punctuation, ":")
+ addStyle(AppTheme.code.punctuation, "=")
+ addStyle(AppTheme.code.punctuation, "\"")
+ addStyle(AppTheme.code.punctuation, "[")
+ addStyle(AppTheme.code.punctuation, "]")
+ addStyle(AppTheme.code.punctuation, "{")
+ addStyle(AppTheme.code.punctuation, "}")
+ addStyle(AppTheme.code.punctuation, "(")
+ addStyle(AppTheme.code.punctuation, ")")
+ addStyle(AppTheme.code.punctuation, ",")
+ addStyle(AppTheme.code.keyword, "fun ")
+ addStyle(AppTheme.code.keyword, "val ")
+ addStyle(AppTheme.code.keyword, "var ")
+ addStyle(AppTheme.code.keyword, "private ")
+ addStyle(AppTheme.code.keyword, "internal ")
+ addStyle(AppTheme.code.keyword, "for ")
+ addStyle(AppTheme.code.keyword, "expect ")
+ addStyle(AppTheme.code.keyword, "actual ")
+ addStyle(AppTheme.code.keyword, "import ")
+ addStyle(AppTheme.code.keyword, "package ")
+ addStyle(AppTheme.code.value, "true")
+ addStyle(AppTheme.code.value, "false")
+ addStyle(AppTheme.code.value, Regex("[0-9]*"))
+ addStyle(AppTheme.code.annotation, Regex("^@[a-zA-Z_]*"))
+ addStyle(AppTheme.code.comment, Regex("^\\s*//.*"))
+ }
+}
+
+private fun AnnotatedString.Builder.addStyle(style: SpanStyle, regexp: String) {
+ addStyle(style, fromLiteral(regexp))
+}
+
+private fun AnnotatedString.Builder.addStyle(style: SpanStyle, regexp: Regex) {
+ for (result in regexp.findAll(toString())) {
+ addStyle(style, result.range.first, result.range.last + 1)
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editors.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editors.kt
new file mode 100644
index 00000000..a6efd020
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editors.kt
@@ -0,0 +1,32 @@
+package org.jetbrains.codeviewer.ui.editor
+
+import androidx.compose.runtime.mutableStateListOf
+import org.jetbrains.codeviewer.platform.File
+import org.jetbrains.codeviewer.util.SingleSelection
+
+class Editors {
+ private val selection = SingleSelection()
+
+ var editors = mutableStateListOf()
+ private set
+
+ val active: Editor? get() = selection.selected as Editor?
+
+ fun open(file: File) {
+ val editor = Editor(file)
+ editor.selection = selection
+ editor.close = {
+ close(editor)
+ }
+ editors.add(editor)
+ editor.activate()
+ }
+
+ private fun close(editor: Editor) {
+ val index = editors.indexOf(editor)
+ editors.remove(editor)
+ if (editor.isActive) {
+ selection.selected = editors.getOrNull(index.coerceAtMost(editors.lastIndex))
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/filetree/FileTree.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/filetree/FileTree.kt
new file mode 100644
index 00000000..e1b6cc8b
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/filetree/FileTree.kt
@@ -0,0 +1,72 @@
+package org.jetbrains.codeviewer.ui.filetree
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import org.jetbrains.codeviewer.platform.File
+import org.jetbrains.codeviewer.ui.editor.Editors
+
+class ExpandableFile(
+ val file: File,
+ val level: Int,
+) {
+ var children: List by mutableStateOf(emptyList())
+ val canExpand: Boolean get() = file.hasChildren
+
+ fun toggleExpanded() {
+ children = if (children.isEmpty()) {
+ file.children
+ .map { ExpandableFile(it, level + 1) }
+ .sortedWith(compareBy({ it.file.isDirectory }, { it.file.name }))
+ .sortedBy { !it.file.isDirectory }
+ } else {
+ emptyList()
+ }
+ }
+}
+
+class FileTree(root: File, private val editors: Editors) {
+ private val expandableRoot = ExpandableFile(root, 0).apply {
+ toggleExpanded()
+ }
+
+ val items: List- get() = expandableRoot.toItems()
+
+ inner class Item constructor(
+ private val file: ExpandableFile
+ ) {
+ val name: String get() = file.file.name
+
+ val level: Int get() = file.level
+
+ val type: ItemType
+ get() = if (file.file.isDirectory) {
+ ItemType.Folder(isExpanded = file.children.isNotEmpty(), canExpand = file.canExpand)
+ } else {
+ ItemType.File(ext = file.file.name.substringAfterLast(".").toLowerCase())
+ }
+
+ fun open() = when (type) {
+ is ItemType.Folder -> file.toggleExpanded()
+ is ItemType.File -> editors.open(file.file)
+ }
+ }
+
+ sealed class ItemType {
+ class Folder(val isExpanded: Boolean, val canExpand: Boolean) : ItemType()
+ class File(val ext: String) : ItemType()
+ }
+
+ private fun ExpandableFile.toItems(): List
- {
+ fun ExpandableFile.addTo(list: MutableList
- ) {
+ list.add(Item(this))
+ for (child in children) {
+ child.addTo(list)
+ }
+ }
+
+ val list = mutableListOf
- ()
+ addTo(list)
+ return list
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/filetree/FileTreeView.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/filetree/FileTreeView.kt
new file mode 100644
index 00000000..00175e7a
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/filetree/FileTreeView.kt
@@ -0,0 +1,128 @@
+package org.jetbrains.codeviewer.ui.filetree
+
+import androidx.compose.foundation.AmbientContentColor
+import androidx.compose.foundation.Text
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.Icon
+import androidx.compose.material.Surface
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.DensityAmbient
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import org.jetbrains.codeviewer.platform.VerticalScrollbar
+import org.jetbrains.codeviewer.platform.pointerMoveFilter
+import org.jetbrains.codeviewer.util.withoutWidthConstraints
+
+@Composable
+fun FileTreeViewTabView() = Surface {
+ Row(
+ Modifier.padding(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ "Files",
+ color = AmbientContentColor.current.copy(alpha = 0.60f),
+ fontSize = 12.sp,
+ modifier = Modifier.padding(horizontal = 4.dp)
+ )
+ }
+}
+
+@Composable
+fun FileTreeView(model: FileTree) = Surface(
+ modifier = Modifier.fillMaxSize()
+) {
+ with(DensityAmbient.current) {
+ Box {
+ val scrollState = rememberLazyListState()
+ val fontSize = 14.sp
+ val lineHeight = fontSize.toDp() * 1.5f
+
+ LazyColumnFor(
+ model.items,
+ modifier = Modifier.fillMaxSize().withoutWidthConstraints(),
+ state = scrollState,
+ itemContent = { FileTreeItemView(fontSize, lineHeight, it) }
+ )
+
+ VerticalScrollbar(
+ Modifier.align(Alignment.CenterEnd),
+ scrollState,
+ model.items.size,
+ lineHeight
+ )
+ }
+ }
+}
+
+@Composable
+private fun FileTreeItemView(fontSize: TextUnit, height: Dp, model: FileTree.Item) = Row(
+ modifier = Modifier
+ .wrapContentHeight()
+ .clickable { model.open() }
+ .padding(start = 24.dp * model.level)
+ .height(height)
+ .fillMaxWidth()
+) {
+ val active = remember { mutableStateOf(false) }
+
+ FileItemIcon(Modifier.align(Alignment.CenterVertically), model)
+ Text(
+ text = model.name,
+ color = if (active.value) AmbientContentColor.current.copy(alpha = 0.60f) else AmbientContentColor.current,
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .clipToBounds()
+ .pointerMoveFilter(
+ onEnter = {
+ active.value = true
+ true
+ },
+ onExit = {
+ active.value = false
+ true
+ }
+ ),
+ softWrap = true,
+ fontSize = fontSize,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1
+ )
+}
+
+@Composable
+private fun FileItemIcon(modifier: Modifier, model: FileTree.Item) = Box(modifier.size(24.dp).padding(4.dp)) {
+ when (val type = model.type) {
+ is FileTree.ItemType.Folder -> when {
+ !type.canExpand -> Unit
+ type.isExpanded -> Icon(Icons.Default.KeyboardArrowDown, tint = AmbientContentColor.current)
+ else -> Icon(Icons.Default.KeyboardArrowRight, tint = AmbientContentColor.current)
+ }
+ is FileTree.ItemType.File -> when (type.ext) {
+ "kt" -> Icon(Icons.Default.Code, tint = Color(0xFF3E86A0))
+ "xml" -> Icon(Icons.Default.Code, tint = Color(0xFFC19C5F))
+ "txt" -> Icon(Icons.Default.Description, tint = Color(0xFF87939A))
+ "md" -> Icon(Icons.Default.Description, tint = Color(0xFF87939A))
+ "gitignore" -> Icon(Icons.Default.BrokenImage, tint = Color(0xFF87939A))
+ "gradle" -> Icon(Icons.Default.Build, tint = Color(0xFF87939A))
+ "kts" -> Icon(Icons.Default.Build, tint = Color(0xFF3E86A0))
+ "properties" -> Icon(Icons.Default.Settings, tint = Color(0xFF62B543))
+ "bat" -> Icon(Icons.Default.Launch, tint = Color(0xFF87939A))
+ else -> Icon(Icons.Default.TextSnippet, tint = Color(0xFF87939A))
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/statusbar/StatusBar.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/statusbar/StatusBar.kt
new file mode 100644
index 00000000..bbed260a
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/statusbar/StatusBar.kt
@@ -0,0 +1,48 @@
+package org.jetbrains.codeviewer.ui.statusbar
+
+import androidx.compose.foundation.AmbientContentColor
+import androidx.compose.foundation.Text
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.Slider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Providers
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.DensityAmbient
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.lerp
+import androidx.compose.ui.unit.sp
+import org.jetbrains.codeviewer.ui.common.Settings
+
+private val MinFontSize = 6.sp
+private val MaxFontSize = 40.sp
+
+@Composable
+fun StatusBar(settings: Settings) = Box(
+ Modifier
+ .height(32.dp)
+ .fillMaxWidth()
+ .padding(4.dp)
+) {
+ Row(Modifier.fillMaxHeight().align(Alignment.CenterEnd)) {
+ Text(
+ text = "Text size",
+ modifier = Modifier.align(Alignment.CenterVertically),
+ color = AmbientContentColor.current.copy(alpha = 0.60f),
+ fontSize = 12.sp
+ )
+
+ Spacer(Modifier.width(8.dp))
+
+ Providers(DensityAmbient provides DensityAmbient.current.scale(0.5f)) {
+ Slider(
+ (settings.fontSize - MinFontSize) / (MaxFontSize - MinFontSize),
+ onValueChange = { settings.fontSize = lerp(MinFontSize, MaxFontSize, it) },
+ modifier = Modifier.width(240.dp).align(Alignment.CenterVertically)
+ )
+ }
+ }
+}
+
+private fun Density.scale(scale: Float) = Density(density * scale, fontScale * scale)
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/LayoutModifiers.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/LayoutModifiers.kt
new file mode 100644
index 00000000..0a054d78
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/LayoutModifiers.kt
@@ -0,0 +1,11 @@
+package org.jetbrains.codeviewer.util
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout
+
+fun Modifier.withoutWidthConstraints() = layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints.copy(maxWidth = Int.MAX_VALUE))
+ layout(constraints.maxWidth, placeable.height) {
+ placeable.place(0, 0)
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/LazyColumnFor.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/LazyColumnFor.kt
new file mode 100644
index 00000000..66e9f581
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/LazyColumnFor.kt
@@ -0,0 +1,33 @@
+package org.jetbrains.codeviewer.util
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.lazy.LazyColumnForIndexed
+import androidx.compose.foundation.lazy.LazyItemScope
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun LazyColumnFor(
+ size: Int,
+ modifier: Modifier = Modifier,
+ state: LazyListState = rememberLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ horizontalGravity: Alignment.Horizontal = Alignment.Start,
+ itemContent: @Composable LazyItemScope.(index: Int) -> Unit
+) = LazyColumnForIndexed(
+ UnitList(size),
+ modifier,
+ state,
+ contentPadding,
+ horizontalGravity,
+) { index, _ ->
+ itemContent(index)
+}
+
+private class UnitList(override val size: Int) : AbstractList() {
+ override fun get(index: Int) = Unit
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/Loadable.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/Loadable.kt
new file mode 100644
index 00000000..a8ed0a80
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/Loadable.kt
@@ -0,0 +1,25 @@
+package org.jetbrains.codeviewer.util
+
+import androidx.compose.runtime.*
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+
+@Composable
+fun loadable(load: suspend () -> T): MutableState {
+ return loadableScoped { load() }
+}
+
+@Composable
+fun loadableScoped(load: suspend CoroutineScope.() -> T): MutableState {
+ val state: MutableState = remember { mutableStateOf(null) }
+ LaunchedTask {
+ try {
+ state.value = load()
+ } catch (e: CancellationException) {
+ // ignore
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ return state
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/SingleSelection.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/SingleSelection.kt
new file mode 100644
index 00000000..1defe47d
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/SingleSelection.kt
@@ -0,0 +1,9 @@
+package org.jetbrains.codeviewer.util
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+
+class SingleSelection {
+ var selected: Any? by mutableStateOf(null)
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/State.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/State.kt
new file mode 100644
index 00000000..118d7e0a
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/State.kt
@@ -0,0 +1,14 @@
+package org.jetbrains.codeviewer.util
+
+import androidx.compose.runtime.MutableState
+
+fun MutableState.afterSet(
+ action: (T) -> Unit
+) = object : MutableState by this {
+ override var value: T
+ get() = this@afterSet.value
+ set(value) {
+ this@afterSet.value = value
+ action(value)
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/TextLines.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/TextLines.kt
new file mode 100644
index 00000000..dbcfc263
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/TextLines.kt
@@ -0,0 +1,6 @@
+package org.jetbrains.codeviewer.util
+
+interface TextLines {
+ val size: Int
+ suspend fun get(index: Int): String
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/VerticalSplitter.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/VerticalSplitter.kt
new file mode 100644
index 00000000..cb6f22f1
--- /dev/null
+++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/util/VerticalSplitter.kt
@@ -0,0 +1,91 @@
+package org.jetbrains.codeviewer.util
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Layout
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import org.jetbrains.codeviewer.platform.cursorForHorizontalResize
+import org.jetbrains.codeviewer.ui.common.AppTheme
+
+@Composable
+fun VerticalSplitable(
+ modifier: Modifier,
+ splitterState: SplitterState,
+ onResize: (delta: Dp) -> Unit,
+ children: @Composable () -> Unit
+) = Layout({
+ children()
+ VerticalSplitter(splitterState, onResize)
+}, modifier, measureBlock = { measurables, constraints ->
+ require(measurables.size == 3)
+
+ val firstPlaceable = measurables[0].measure(constraints.copy(minWidth = 0))
+ val secondWidth = constraints.maxWidth - firstPlaceable.width
+ val secondPlaceable = measurables[1].measure(
+ Constraints(
+ minWidth = secondWidth,
+ maxWidth = secondWidth,
+ minHeight = constraints.maxHeight,
+ maxHeight = constraints.maxHeight
+ )
+ )
+ val splitterPlaceable = measurables[2].measure(constraints)
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ firstPlaceable.place(0, 0)
+ secondPlaceable.place(firstPlaceable.width, 0)
+ splitterPlaceable.place(firstPlaceable.width, 0)
+ }
+})
+
+class SplitterState {
+ var isResizing by mutableStateOf(false)
+ var isResizeEnabled by mutableStateOf(true)
+}
+
+@Composable
+fun VerticalSplitter(
+ splitterState: SplitterState,
+ onResize: (delta: Dp) -> Unit,
+ color: Color = AppTheme.colors.backgroundDark
+) = Box {
+ Box(
+ Modifier
+ .width(8.dp)
+ .fillMaxHeight()
+ .run {
+ if (splitterState.isResizeEnabled) {
+ this.
+ draggable(
+ Orientation.Horizontal,
+ startDragImmediately = true,
+ onDragStarted = { splitterState.isResizing = true },
+ onDragStopped = { splitterState.isResizing = false }
+ ) {
+ onResize(it.toDp())
+ }
+ .cursorForHorizontalResize()
+ } else {
+ this
+ }
+ }
+ )
+
+ Box(
+ Modifier
+ .width(1.dp)
+ .fillMaxHeight()
+ .background(color)
+ )
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_bold.ttf b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_bold.ttf
new file mode 100644
index 00000000..5dc6ec24
Binary files /dev/null and b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_bold.ttf differ
diff --git a/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_bold_italic.ttf b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_bold_italic.ttf
new file mode 100644
index 00000000..8a36bccf
Binary files /dev/null and b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_bold_italic.ttf differ
diff --git a/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_extrabold.ttf b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_extrabold.ttf
new file mode 100644
index 00000000..bba598e0
Binary files /dev/null and b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_extrabold.ttf differ
diff --git a/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_extrabold_italic.ttf b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_extrabold_italic.ttf
new file mode 100644
index 00000000..ff89d25d
Binary files /dev/null and b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_extrabold_italic.ttf differ
diff --git a/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_italic.ttf b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_italic.ttf
new file mode 100644
index 00000000..44e1f4a7
Binary files /dev/null and b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_italic.ttf differ
diff --git a/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_medium.ttf b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_medium.ttf
new file mode 100644
index 00000000..017b81fb
Binary files /dev/null and b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_medium.ttf differ
diff --git a/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_medium_italic.ttf b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_medium_italic.ttf
new file mode 100644
index 00000000..6da1d175
Binary files /dev/null and b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_medium_italic.ttf differ
diff --git a/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_regular.ttf b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_regular.ttf
new file mode 100644
index 00000000..7db854fd
Binary files /dev/null and b/examples/codeviewer/common/src/commonMain/resources/font/jetbrainsmono_regular.ttf differ
diff --git a/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/File.kt b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/File.kt
new file mode 100644
index 00000000..9dfe0a9c
--- /dev/null
+++ b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/File.kt
@@ -0,0 +1,5 @@
+@file:Suppress("NewApi")
+
+package org.jetbrains.codeviewer.platform
+
+actual val HomeFolder: File get() = java.io.File(System.getProperty("user.home")).toProjectFile()
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt
new file mode 100644
index 00000000..dbd45841
--- /dev/null
+++ b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt
@@ -0,0 +1,33 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.desktop.AppWindowAmbient
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.pointerMoveFilter
+import java.awt.Cursor
+
+actual fun Modifier.pointerMoveFilter(
+ onEnter: () -> Boolean,
+ onExit: () -> Boolean,
+ onMove: (Offset) -> Boolean
+): Modifier = this.pointerMoveFilter(onEnter = onEnter, onExit = onExit, onMove = onMove)
+
+actual fun Modifier.cursorForHorizontalResize(): Modifier = composed {
+ var isHover by remember { mutableStateOf(false) }
+
+ if (isHover) {
+ AppWindowAmbient.current!!.window.cursor = Cursor(Cursor.E_RESIZE_CURSOR)
+ } else {
+ AppWindowAmbient.current!!.window.cursor = Cursor.getDefaultCursor()
+ }
+
+ pointerMoveFilter(
+ onEnter = { isHover = true; true },
+ onExit = { isHover = false; true }
+ )
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt
new file mode 100644
index 00000000..6dd72562
--- /dev/null
+++ b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Resources.kt
@@ -0,0 +1,25 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.ImageAsset
+import androidx.compose.ui.graphics.asImageAsset
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jetbrains.skija.Image
+import java.io.InputStream
+import java.net.URL
+
+@Composable
+actual fun imageResource(res: String) = androidx.compose.ui.res.imageResource("drawable/$res.png")
+
+actual suspend fun imageFromUrl(url: String): ImageAsset = withContext(Dispatchers.IO) {
+ val bytes = URL(url).openStream().buffered().use(InputStream::readBytes)
+ Image.makeFromEncoded(bytes).asImageAsset()
+}
+
+@Composable
+actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font =
+ androidx.compose.ui.text.platform.font(name, "font/$res.ttf", weight, style)
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt
new file mode 100644
index 00000000..5c1ae84f
--- /dev/null
+++ b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Scrollbar.kt
@@ -0,0 +1,48 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.LazyScrollbarAdapter
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.rememberScrollbarAdapter
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.DensityAmbient
+import androidx.compose.ui.unit.Dp
+
+@Composable
+actual fun VerticalScrollbar(
+ modifier: Modifier,
+ scrollState: ScrollState
+) = androidx.compose.foundation.VerticalScrollbar(
+ modifier,
+ adapter = rememberScrollbarAdapter(scrollState)
+)
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+actual fun VerticalScrollbar(
+ modifier: Modifier,
+ scrollState: LazyListState,
+ itemCount: Int,
+ averageItemSize: Dp
+) = androidx.compose.foundation.VerticalScrollbar(
+ modifier,
+ adapter = rememberScrollbarAdapterFixed(scrollState, itemCount, averageItemSize)
+)
+
+// TODO/migrateToMaster should be fixed in androidx-master-dev
+@Composable
+fun rememberScrollbarAdapterFixed(
+ scrollState: LazyListState,
+ itemCount: Int,
+ averageItemSize: Dp
+): LazyScrollbarAdapter {
+ val density = DensityAmbient.current
+ return remember(density, scrollState, itemCount, averageItemSize) {
+ with(density) {
+ LazyScrollbarAdapter(scrollState, itemCount, averageItemSize.toPx())
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt
new file mode 100644
index 00000000..2b30e0ee
--- /dev/null
+++ b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Selection.kt
@@ -0,0 +1,26 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.DesktopSelectionContainer
+import androidx.compose.ui.selection.Selection
+
+@Composable
+actual fun SelectionContainer(children: @Composable () -> Unit) {
+ val selection = remember { mutableStateOf(null) }
+ DesktopSelectionContainer(
+ selection = selection.value,
+ onSelectionChange = { selection.value = it },
+ children = children
+ )
+}
+
+@Composable
+actual fun WithoutSelection(children: @Composable () -> Unit) {
+ androidx.compose.ui.selection.SelectionContainer(
+ selection = null,
+ onSelectionChange = {},
+ children = children
+ )
+}
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt
new file mode 100644
index 00000000..c3da0e1b
--- /dev/null
+++ b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt
@@ -0,0 +1,7 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.desktop.DesktopTheme
+import androidx.compose.runtime.Composable
+
+@Composable
+actual fun PlatformTheme(content: @Composable () -> Unit) = DesktopTheme(content = content)
\ No newline at end of file
diff --git a/examples/codeviewer/common/src/jvmMain/kotlin/org/jetbrains/codeviewer/platform/JvmFile.kt b/examples/codeviewer/common/src/jvmMain/kotlin/org/jetbrains/codeviewer/platform/JvmFile.kt
new file mode 100644
index 00000000..49c83db7
--- /dev/null
+++ b/examples/codeviewer/common/src/jvmMain/kotlin/org/jetbrains/codeviewer/platform/JvmFile.kt
@@ -0,0 +1,141 @@
+package org.jetbrains.codeviewer.platform
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import kotlinx.coroutines.*
+import org.jetbrains.codeviewer.util.TextLines
+import java.io.FileInputStream
+import java.io.IOException
+import java.io.RandomAccessFile
+import java.nio.channels.FileChannel
+
+fun java.io.File.toProjectFile(): File = object : File {
+ override val name: String get() = this@toProjectFile.name
+
+ override val isDirectory: Boolean get() = this@toProjectFile.isDirectory
+
+ override val children: List
+ get() = this@toProjectFile
+ .listFiles()
+ .orEmpty()
+ .map { it.toProjectFile() }
+
+ override val hasChildren: Boolean
+ get() = isDirectory && listFiles()?.size ?: 0 > 0
+
+ override suspend fun readLines(backgroundScope: CoroutineScope): TextLines {
+ val linePositions = IntList()
+ var size by mutableStateOf(0)
+
+ val refreshJob = backgroundScope.launch {
+ delay(100)
+ size = linePositions.size
+ while (true) {
+ delay(1000)
+ size = linePositions.size
+ }
+ }
+
+ backgroundScope.launch {
+ readLinePositions(linePositions)
+ refreshJob.cancel()
+ size = linePositions.size
+ }
+
+ return object : TextLines {
+ override val size get() = size
+
+ override suspend fun get(index: Int): String {
+ return withContext(Dispatchers.IO) {
+ val position = linePositions[index]
+ try {
+ RandomAccessFile(this@toProjectFile, "rws").use {
+ it.seek(position.toLong())
+ String(
+ it.readLine()
+ .toCharArray()
+ .map(Char::toByte)
+ .toByteArray(),
+ Charsets.UTF_8
+ )
+ }
+ } catch (e: IOException) {
+ e.printStackTrace()
+ ""
+ }
+ }
+ }
+ }
+ }
+}
+
+@Suppress("BlockingMethodInNonBlockingContext")
+private suspend fun java.io.File.readLinePositions(list: IntList) = withContext(Dispatchers.IO) {
+ require(length() <= Int.MAX_VALUE) {
+ "Files with size over ${Int.MAX_VALUE} aren't supported"
+ }
+
+ val averageLineLength = 200
+ // linePositions can be very big, so we are using IntArray instead of List
+ list.clear(length().toInt() / averageLineLength)
+
+ var isBeginOfLine = true
+ var position = 0L
+
+ try {
+ FileInputStream(this@readLinePositions).use {
+ val channel = it.channel
+ val ib = channel.map(
+ FileChannel.MapMode.READ_ONLY, 0, channel.size()
+ )
+ while (ib.hasRemaining()) {
+ val byte = ib.get()
+ if (isBeginOfLine) {
+ list.add(position.toInt())
+ }
+ isBeginOfLine = byte.toChar() == '\n'
+ position++
+ }
+ }
+ } catch (e: IOException) {
+ e.printStackTrace()
+ list.clear(1)
+ list.add(0)
+ }
+
+ list.compact()
+}
+
+private class IntList(initialCapacity: Int = 16) {
+ @Volatile
+ private var array = IntArray(initialCapacity)
+
+ @Volatile
+ var size: Int = 0
+ private set
+
+ fun clear(capacity: Int) {
+ array = IntArray(capacity)
+ size = 0
+ }
+
+ fun add(value: Int) {
+ if (size == array.size) {
+ doubleCapacity()
+ }
+ array[size++] = value
+ }
+
+ operator fun get(index: Int) = array[index]
+
+ private fun doubleCapacity() {
+ val newArray = IntArray(array.size * 2 + 1)
+ System.arraycopy(array, 0, newArray, 0, size)
+ array = newArray
+ }
+
+ fun compact() {
+ array = array.copyOfRange(0, size)
+ }
+}
\ No newline at end of file
diff --git a/examples/codeviewer/desktop/build.gradle.kts b/examples/codeviewer/desktop/build.gradle.kts
new file mode 100644
index 00000000..bde67132
--- /dev/null
+++ b/examples/codeviewer/desktop/build.gradle.kts
@@ -0,0 +1,27 @@
+import org.jetbrains.compose.compose
+
+plugins {
+ kotlin("multiplatform")
+ id("org.jetbrains.compose")
+ java
+ application
+}
+
+kotlin {
+ jvm {
+ withJava()
+ }
+
+ sourceSets {
+ named("jvmMain") {
+ dependencies {
+ implementation(compose.desktop.all)
+ implementation(project(":common"))
+ }
+ }
+ }
+}
+
+application {
+ mainClassName = "org.jetbrains.codeviewer.MainKt"
+}
\ No newline at end of file
diff --git a/examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt b/examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt
new file mode 100644
index 00000000..6732985d
--- /dev/null
+++ b/examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt
@@ -0,0 +1,24 @@
+package org.jetbrains.codeviewer
+
+import androidx.compose.desktop.Window
+import androidx.compose.foundation.layout.ExperimentalLayout
+import androidx.compose.ui.unit.IntSize
+import org.jetbrains.codeviewer.ui.MainView
+import java.awt.image.BufferedImage
+import javax.imageio.ImageIO
+
+@OptIn(ExperimentalLayout::class)
+fun main() = Window(
+ title = "Code Viewer",
+ size = IntSize(1280, 768),
+ icon = loadImageResource("ic_launcher.png")
+) {
+ MainView()
+}
+
+@Suppress("SameParameterValue")
+private fun loadImageResource(path: String): BufferedImage {
+ val resource = Thread.currentThread().contextClassLoader.getResource(path)
+ requireNotNull(resource) { "Resource $path not found" }
+ return resource.openStream().use(ImageIO::read)
+}
\ No newline at end of file
diff --git a/examples/codeviewer/desktop/src/jvmMain/resources/ic_launcher.png b/examples/codeviewer/desktop/src/jvmMain/resources/ic_launcher.png
new file mode 100644
index 00000000..db540cf9
Binary files /dev/null and b/examples/codeviewer/desktop/src/jvmMain/resources/ic_launcher.png differ
diff --git a/examples/codeviewer/gradle.properties b/examples/codeviewer/gradle.properties
new file mode 100644
index 00000000..4d15d015
--- /dev/null
+++ b/examples/codeviewer/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
\ No newline at end of file
diff --git a/examples/codeviewer/gradle/wrapper/gradle-wrapper.jar b/examples/codeviewer/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..62d4c053
Binary files /dev/null and b/examples/codeviewer/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/codeviewer/gradle/wrapper/gradle-wrapper.properties b/examples/codeviewer/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..ac33e994
--- /dev/null
+++ b/examples/codeviewer/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/examples/codeviewer/gradlew b/examples/codeviewer/gradlew
new file mode 100644
index 00000000..2fe81a7d
--- /dev/null
+++ b/examples/codeviewer/gradlew
@@ -0,0 +1,183 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/examples/codeviewer/gradlew.bat b/examples/codeviewer/gradlew.bat
new file mode 100644
index 00000000..9618d8d9
--- /dev/null
+++ b/examples/codeviewer/gradlew.bat
@@ -0,0 +1,100 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/examples/codeviewer/settings.gradle.kts b/examples/codeviewer/settings.gradle.kts
new file mode 100644
index 00000000..9a0d554e
--- /dev/null
+++ b/examples/codeviewer/settings.gradle.kts
@@ -0,0 +1 @@
+include(":common", ":android", ":desktop")