Igor Demin
4 years ago
72 changed files with 2343 additions and 0 deletions
@ -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 |
@ -0,0 +1,21 @@
|
||||
<component name="ProjectRunConfigurationManager"> |
||||
<configuration default="false" name="desktop" type="GradleRunConfiguration" factoryName="Gradle"> |
||||
<ExternalSystemSettings> |
||||
<option name="executionName" /> |
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/desktop" /> |
||||
<option name="externalSystemIdString" value="GRADLE" /> |
||||
<option name="scriptParameters" value="" /> |
||||
<option name="taskDescriptions"> |
||||
<list /> |
||||
</option> |
||||
<option name="taskNames"> |
||||
<list> |
||||
<option value="run" /> |
||||
</list> |
||||
</option> |
||||
<option name="vmOptions" value="" /> |
||||
</ExternalSystemSettings> |
||||
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled> |
||||
<method v="2" /> |
||||
</configuration> |
||||
</component> |
@ -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' |
@ -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")) |
||||
} |
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
package="org.jetbrains.codeviewer"> |
||||
|
||||
<application |
||||
android:allowBackup="true" |
||||
android:icon="@mipmap/ic_launcher" |
||||
android:label="@string/app_name" |
||||
android:roundIcon="@mipmap/ic_launcher_round" |
||||
android:supportsRtl="true" |
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"> |
||||
<activity |
||||
android:name="MainActivity" |
||||
android:label="@string/app_name"> |
||||
<intent-filter> |
||||
<action android:name="android.intent.action.MAIN" /> |
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" /> |
||||
</intent-filter> |
||||
</activity> |
||||
</application> |
||||
|
||||
</manifest> |
@ -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) |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:width="108dp" |
||||
android:height="108dp" |
||||
android:viewportWidth="108" |
||||
android:viewportHeight="108" |
||||
android:tint="#FFFFFF"> |
||||
<group android:scaleX="2.61" |
||||
android:scaleY="2.61" |
||||
android:translateX="22.68" |
||||
android:translateY="22.68"> |
||||
<path |
||||
android:fillColor="@android:color/white" |
||||
android:pathData="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z"/> |
||||
</group> |
||||
</vector> |
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<background android:drawable="@color/ic_launcher_background"/> |
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/> |
||||
</adaptive-icon> |
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<background android:drawable="@color/ic_launcher_background"/> |
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/> |
||||
</adaptive-icon> |
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources> |
||||
<color name="ic_launcher_background">#3C3F41</color> |
||||
</resources> |
@ -0,0 +1,3 @@
|
||||
<resources> |
||||
<string name="app_name">Code Viewer</string> |
||||
</resources> |
@ -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") |
||||
} |
||||
} |
@ -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<com.android.build.gradle.LibraryExtension>()!!.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") |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest package="org.jetbrains.codeviewer.common"/> |
@ -0,0 +1,4 @@
|
||||
package org.jetbrains.codeviewer.platform |
||||
|
||||
lateinit var _HomeFolder: java.io.File |
||||
actual val HomeFolder: File get() = _HomeFolder.toProjectFile() |
@ -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 |
@ -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) |
||||
} |
@ -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 |
@ -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<Selection?>(null) } |
||||
SelectionContainer( |
||||
selection = selection.value, |
||||
onSelectionChange = { selection.value = it }, |
||||
children = children |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
actual fun WithoutSelection(children: @Composable () -> Unit) { |
||||
SelectionContainer( |
||||
selection = null, |
||||
onSelectionChange = {}, |
||||
children = children |
||||
) |
||||
} |
@ -0,0 +1,6 @@
|
||||
package org.jetbrains.codeviewer.platform |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
|
||||
@Composable |
||||
actual fun PlatformTheme(content: @Composable () -> Unit) = content() |
@ -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<File> |
||||
val hasChildren: Boolean |
||||
|
||||
suspend fun readLines(backgroundScope: CoroutineScope): TextLines |
||||
} |
@ -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 |
@ -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 |
@ -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 |
||||
) |
@ -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) |
@ -0,0 +1,6 @@
|
||||
package org.jetbrains.codeviewer.platform |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
|
||||
@Composable |
||||
expect fun PlatformTheme(content: @Composable () -> Unit) |
@ -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 |
||||
) |
@ -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) |
||||
) |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
) |
||||
) |
||||
} |
@ -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 |
||||
} |
@ -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)) |
||||
) |
||||
} |
@ -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<String>, val isCode: Boolean) |
||||
} |
||||
|
||||
fun Editor(file: File) = Editor( |
||||
fileName = file.name |
||||
) { backgroundScope -> |
||||
val textLines = file.readLines(backgroundScope) |
||||
val indexToEditedText = mutableMapOf<Int, String>() |
||||
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) |
||||
) |
||||
} |
||||
} |
@ -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) |
||||
) |
||||
} |
||||
} |
@ -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) |
||||
) |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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<Editor>() |
||||
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)) |
||||
} |
||||
} |
||||
} |
@ -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<ExpandableFile> 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<Item> 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<Item> { |
||||
fun ExpandableFile.addTo(list: MutableList<Item>) { |
||||
list.add(Item(this)) |
||||
for (child in children) { |
||||
child.addTo(list) |
||||
} |
||||
} |
||||
|
||||
val list = mutableListOf<Item>() |
||||
addTo(list) |
||||
return list |
||||
} |
||||
} |
@ -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)) |
||||
} |
||||
} |
||||
} |
@ -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) |
@ -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) |
||||
} |
||||
} |
@ -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<Unit>() { |
||||
override fun get(index: Int) = Unit |
||||
} |
@ -0,0 +1,25 @@
|
||||
package org.jetbrains.codeviewer.util |
||||
|
||||
import androidx.compose.runtime.* |
||||
import kotlinx.coroutines.CancellationException |
||||
import kotlinx.coroutines.CoroutineScope |
||||
|
||||
@Composable |
||||
fun <T : Any> loadable(load: suspend () -> T): MutableState<T?> { |
||||
return loadableScoped { load() } |
||||
} |
||||
|
||||
@Composable |
||||
fun <T : Any> loadableScoped(load: suspend CoroutineScope.() -> T): MutableState<T?> { |
||||
val state: MutableState<T?> = remember { mutableStateOf(null) } |
||||
LaunchedTask { |
||||
try { |
||||
state.value = load() |
||||
} catch (e: CancellationException) { |
||||
// ignore |
||||
} catch (e: Exception) { |
||||
e.printStackTrace() |
||||
} |
||||
} |
||||
return state |
||||
} |
@ -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) |
||||
} |
@ -0,0 +1,14 @@
|
||||
package org.jetbrains.codeviewer.util |
||||
|
||||
import androidx.compose.runtime.MutableState |
||||
|
||||
fun <T> MutableState<T>.afterSet( |
||||
action: (T) -> Unit |
||||
) = object : MutableState<T> by this { |
||||
override var value: T |
||||
get() = this@afterSet.value |
||||
set(value) { |
||||
this@afterSet.value = value |
||||
action(value) |
||||
} |
||||
} |
@ -0,0 +1,6 @@
|
||||
package org.jetbrains.codeviewer.util |
||||
|
||||
interface TextLines { |
||||
val size: Int |
||||
suspend fun get(index: Int): String |
||||
} |
@ -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) |
||||
) |
||||
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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() |
@ -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 } |
||||
) |
||||
} |
@ -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) |
@ -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()) |
||||
} |
||||
} |
||||
} |
@ -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<Selection?>(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 |
||||
) |
||||
} |
@ -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) |
@ -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<File> |
||||
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() |
||||
"<Error on opening the file>" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@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<Long> |
||||
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) |
||||
} |
||||
} |
@ -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" |
||||
} |
@ -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) |
||||
} |
After Width: | Height: | Size: 9.1 KiB |
@ -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 |
Binary file not shown.
@ -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 |
@ -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" "$@" |
@ -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 |
Loading…
Reference in new issue