Nikita Lipsky
2 years ago
committed by
GitHub
66 changed files with 2152 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,20 @@
|
||||
MPP Code Viewer example for desktop/android written in Multiplatform Compose library. |
||||
|
||||
### Running desktop application |
||||
|
||||
* To run, launch command: `./gradlew :desktop:run` |
||||
* Or choose **desktop** configuration in IDE and run it. |
||||
![desktop-run-configuration.png](screenshots/desktop-run-configuration.png) |
||||
|
||||
### Building native desktop distribution |
||||
``` |
||||
./gradlew :desktop:packageDistributionForCurrentOS |
||||
# outputs are written to desktop/build/compose/binaries |
||||
``` |
||||
|
||||
### Installing Android application on device/emulator |
||||
``` |
||||
./gradlew installDebug |
||||
``` |
||||
|
||||
![Desktop](screenshots/codeviewer.png) |
@ -0,0 +1,26 @@
|
||||
plugins { |
||||
id("com.android.application") |
||||
kotlin("android") |
||||
id("org.jetbrains.compose") |
||||
} |
||||
|
||||
android { |
||||
compileSdk = 32 |
||||
|
||||
defaultConfig { |
||||
minSdk = 26 |
||||
targetSdk = 32 |
||||
versionCode = 1 |
||||
versionName = "1.0" |
||||
} |
||||
|
||||
compileOptions { |
||||
sourceCompatibility = JavaVersion.VERSION_11 |
||||
targetCompatibility = JavaVersion.VERSION_11 |
||||
} |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(project(":common")) |
||||
implementation("androidx.activity:activity-compose:1.5.0") |
||||
} |
@ -0,0 +1,24 @@
|
||||
<?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:exported="true" |
||||
android:name="MainActivity" |
||||
> |
||||
<intent-filter> |
||||
<action android:name="android.intent.action.MAIN" /> |
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" /> |
||||
</intent-filter> |
||||
</activity> |
||||
</application> |
||||
|
||||
</manifest> |
@ -0,0 +1,183 @@
|
||||
/** |
||||
* This file is an example (we can open it in android application) |
||||
*/ |
||||
|
||||
package org.jetbrains.codeviewer.ui.editor |
||||
|
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.layout.* |
||||
import androidx.compose.foundation.text.selection.DisableSelection |
||||
import androidx.compose.material.AmbientContentColor |
||||
import androidx.compose.material.CircularProgressIndicator |
||||
import androidx.compose.material.Surface |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.key |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.draw.alpha |
||||
import androidx.compose.ui.platform.LocalDensity |
||||
import androidx.compose.ui.text.* |
||||
import androidx.compose.ui.unit.dp |
||||
import org.jetbrains.codeviewer.platform.SelectionContainer |
||||
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.loadableScoped |
||||
import org.jetbrains.codeviewer.util.withoutWidthConstraints |
||||
import kotlin.text.Regex.Companion.fromLiteral |
||||
|
||||
@Composable |
||||
fun EditorView(model: Editor, settings: Settings) = key(model) { |
||||
with (LocalDensity.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 maxNumber = 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), maxNumber, line!!, settings) |
||||
} |
||||
} |
||||
} |
||||
) |
||||
|
||||
VerticalScrollbar( |
||||
Modifier.align(Alignment.CenterEnd), |
||||
scrollState, |
||||
lines.size, |
||||
lineHeight |
||||
) |
||||
} |
||||
} |
||||
|
||||
// Поддержка русского языка |
||||
// دعم اللغة العربية |
||||
// 中文支持 |
||||
@Composable |
||||
private fun Line(modifier: Modifier, maxNumber: String, line: Editor.Line, settings: Settings) { |
||||
Row(modifier = modifier) { |
||||
DisableSelection { |
||||
Box { |
||||
LineNumber(maxNumber, Modifier.alpha(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) = buildAnnotatedString { |
||||
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 |
||||
|
||||
import android.os.Bundle |
||||
import androidx.activity.compose.setContent |
||||
import androidx.appcompat.app.AppCompatActivity |
||||
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,18 @@
|
||||
plugins { |
||||
// this is necessary to avoid the plugins to be loaded multiple times |
||||
// in each subproject's classloader |
||||
kotlin("jvm") apply false |
||||
kotlin("multiplatform") apply false |
||||
kotlin("android") apply false |
||||
id("com.android.application") apply false |
||||
id("com.android.library") apply false |
||||
id("org.jetbrains.compose") apply false |
||||
} |
||||
|
||||
subprojects { |
||||
repositories { |
||||
google() |
||||
mavenCentral() |
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") |
||||
} |
||||
} |
@ -0,0 +1,50 @@
|
||||
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.5.1") |
||||
api("androidx.core:core-ktx:1.8.0") |
||||
} |
||||
} |
||||
named("desktopMain") { |
||||
kotlin.srcDirs("src/jvmMain/kotlin") |
||||
dependencies { |
||||
api(compose.desktop.common) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
android { |
||||
compileSdk = 32 |
||||
|
||||
defaultConfig { |
||||
minSdk = 26 |
||||
targetSdk = 32 |
||||
} |
||||
|
||||
sourceSets { |
||||
named("main") { |
||||
manifest.srcFile("src/androidMain/AndroidManifest.xml") |
||||
res.srcDirs("src/androidMain/res", "src/commonMain/resources") |
||||
} |
||||
} |
||||
} |
@ -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,6 @@
|
||||
package org.jetbrains.codeviewer.platform |
||||
|
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.geometry.Offset |
||||
|
||||
actual fun Modifier.cursorForHorizontalResize() = this |
@ -0,0 +1,16 @@
|
||||
package org.jetbrains.codeviewer.platform |
||||
|
||||
import android.annotation.SuppressLint |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.platform.LocalContext |
||||
import androidx.compose.ui.text.font.Font |
||||
import androidx.compose.ui.text.font.FontStyle |
||||
import androidx.compose.ui.text.font.FontWeight |
||||
|
||||
@SuppressLint("DiscouragedApi") |
||||
@Composable |
||||
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font { |
||||
val context = LocalContext.current |
||||
val id = context.resources.getIdentifier(res, "font", context.packageName) |
||||
return Font(id, weight, style) |
||||
} |
@ -0,0 +1,18 @@
|
||||
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 |
||||
|
||||
@Composable |
||||
actual fun VerticalScrollbar( |
||||
modifier: Modifier, |
||||
scrollState: ScrollState |
||||
) = Unit |
||||
|
||||
@Composable |
||||
actual fun VerticalScrollbar( |
||||
modifier: Modifier, |
||||
scrollState: LazyListState |
||||
) = Unit |
@ -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 |
||||
|
||||
fun readLines(scope: CoroutineScope): TextLines |
||||
} |
@ -0,0 +1,5 @@
|
||||
package org.jetbrains.codeviewer.platform |
||||
|
||||
import androidx.compose.ui.Modifier |
||||
|
||||
expect fun Modifier.cursorForHorizontalResize(): Modifier |
@ -0,0 +1,9 @@
|
||||
package org.jetbrains.codeviewer.platform |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.text.font.Font |
||||
import androidx.compose.ui.text.font.FontStyle |
||||
import androidx.compose.ui.text.font.FontWeight |
||||
|
||||
@Composable |
||||
expect fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font |
@ -0,0 +1,19 @@
|
||||
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 |
||||
) |
@ -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,107 @@
|
||||
package org.jetbrains.codeviewer.ui |
||||
|
||||
import androidx.compose.animation.core.Spring.StiffnessLow |
||||
import androidx.compose.animation.core.SpringSpec |
||||
import androidx.compose.animation.core.animateDpAsState |
||||
import androidx.compose.animation.core.animateFloatAsState |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.layout.* |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.LocalContentColor |
||||
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.graphics.graphicsLayer |
||||
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.VerticalSplittable |
||||
|
||||
@Composable |
||||
fun CodeViewerView(model: CodeViewer) { |
||||
val panelState = remember { PanelState() } |
||||
|
||||
val animatedSize = if (panelState.splitter.isResizing) { |
||||
if (panelState.isExpanded) panelState.expandedSize else panelState.collapsedSize |
||||
} else { |
||||
animateDpAsState( |
||||
if (panelState.isExpanded) panelState.expandedSize else panelState.collapsedSize, |
||||
SpringSpec(stiffness = StiffnessLow) |
||||
).value |
||||
} |
||||
|
||||
VerticalSplittable( |
||||
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 by animateFloatAsState(if (state.isExpanded) 1f else 0f, SpringSpec(stiffness = StiffnessLow)) |
||||
|
||||
Box(modifier) { |
||||
Box(Modifier.fillMaxSize().graphicsLayer(alpha = alpha)) { |
||||
content() |
||||
} |
||||
|
||||
Icon( |
||||
if (state.isExpanded) Icons.Default.ArrowBack else Icons.Default.ArrowForward, |
||||
contentDescription = if (state.isExpanded) "Collapse" else "Expand", |
||||
tint = LocalContentColor.current, |
||||
modifier = Modifier |
||||
.padding(top = 4.dp) |
||||
.width(24.dp) |
||||
.clickable { |
||||
state.isExpanded = !state.isExpanded |
||||
} |
||||
.padding(4.dp) |
||||
.align(Alignment.TopEnd) |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,35 @@
|
||||
package org.jetbrains.codeviewer.ui |
||||
|
||||
import androidx.compose.foundation.text.selection.DisableSelection |
||||
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.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() |
||||
) |
||||
} |
||||
|
||||
DisableSelection { |
||||
MaterialTheme( |
||||
colors = AppTheme.colors.material |
||||
) { |
||||
Surface { |
||||
CodeViewerView(codeViewer) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,64 @@
|
||||
package org.jetbrains.codeviewer.ui.common |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.text.font.FontFamily |
||||
import androidx.compose.ui.text.font.FontStyle |
||||
import androidx.compose.ui.text.font.FontWeight |
||||
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,60 @@
|
||||
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.EmptyTextLines |
||||
import org.jetbrains.codeviewer.util.SingleSelection |
||||
|
||||
class Editor( |
||||
val fileName: String, |
||||
val lines: (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 |
||||
operator 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 = try { |
||||
file.readLines(backgroundScope) |
||||
} catch (e: Throwable) { |
||||
e.printStackTrace() |
||||
EmptyTextLines |
||||
} |
||||
val isCode = file.name.endsWith(".kt", ignoreCase = true) |
||||
|
||||
fun content(index: Int): Editor.Content { |
||||
val text = textLines.get(index) |
||||
val state = mutableStateOf(text) |
||||
return Editor.Content(state, isCode) |
||||
} |
||||
|
||||
object : Editor.Lines { |
||||
override val size get() = textLines.size |
||||
|
||||
override fun get(index: Int) = Editor.Line( |
||||
number = index + 1, |
||||
content = content(index) |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,35 @@
|
||||
package org.jetbrains.codeviewer.ui.editor |
||||
|
||||
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.LocalContentColor |
||||
import androidx.compose.material.Text |
||||
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, |
||||
contentDescription = null, |
||||
tint = LocalContentColor.current.copy(alpha = 0.60f), |
||||
modifier = Modifier.align(Alignment.CenterHorizontally) |
||||
) |
||||
|
||||
Text( |
||||
"To view file open it from the file tree", |
||||
color = LocalContentColor.current.copy(alpha = 0.60f), |
||||
fontSize = 20.sp, |
||||
modifier = Modifier.align(Alignment.CenterHorizontally).padding(16.dp) |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,78 @@
|
||||
package org.jetbrains.codeviewer.ui.editor |
||||
|
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.horizontalScroll |
||||
import androidx.compose.foundation.interaction.MutableInteractionSource |
||||
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.foundation.rememberScrollState |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.LocalContentColor |
||||
import androidx.compose.material.Surface |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.filled.Close |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.remember |
||||
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) = Row(Modifier.horizontalScroll(rememberScrollState())) { |
||||
for (editor in model.editors) { |
||||
EditorTabView(editor) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun EditorTabView(model: Editor) = Surface( |
||||
color = if (model.isActive) { |
||||
AppTheme.colors.backgroundDark |
||||
} else { |
||||
Color.Transparent |
||||
} |
||||
) { |
||||
Row( |
||||
Modifier |
||||
.clickable(remember(::MutableInteractionSource), indication = null) { |
||||
model.activate() |
||||
} |
||||
.padding(4.dp), |
||||
verticalAlignment = Alignment.CenterVertically |
||||
) { |
||||
Text( |
||||
model.fileName, |
||||
color = LocalContentColor.current, |
||||
fontSize = 12.sp, |
||||
modifier = Modifier.padding(horizontal = 4.dp) |
||||
) |
||||
|
||||
val close = model.close |
||||
|
||||
if (close != null) { |
||||
Icon( |
||||
Icons.Default.Close, |
||||
tint = LocalContentColor.current, |
||||
contentDescription = "Close", |
||||
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,185 @@
|
||||
package org.jetbrains.codeviewer.ui.editor |
||||
|
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.layout.* |
||||
import androidx.compose.foundation.lazy.LazyColumn |
||||
import androidx.compose.foundation.lazy.rememberLazyListState |
||||
import androidx.compose.foundation.text.selection.DisableSelection |
||||
import androidx.compose.foundation.text.selection.SelectionContainer |
||||
import androidx.compose.material.CircularProgressIndicator |
||||
import androidx.compose.material.LocalContentColor |
||||
import androidx.compose.material.Surface |
||||
import androidx.compose.material.Text |
||||
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.alpha |
||||
import androidx.compose.ui.platform.LocalDensity |
||||
import androidx.compose.ui.text.AnnotatedString |
||||
import androidx.compose.ui.text.SpanStyle |
||||
import androidx.compose.ui.text.buildAnnotatedString |
||||
import androidx.compose.ui.text.withStyle |
||||
import androidx.compose.ui.unit.dp |
||||
import org.jetbrains.codeviewer.platform.VerticalScrollbar |
||||
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.loadableScoped |
||||
import org.jetbrains.codeviewer.util.withoutWidthConstraints |
||||
import kotlin.text.Regex.Companion.fromLiteral |
||||
|
||||
@Composable |
||||
fun EditorView(model: Editor, settings: Settings) = key(model) { |
||||
with (LocalDensity.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(LocalDensity.current) { |
||||
val maxNum = remember(lines.lineNumberDigitCount) { |
||||
(1..lines.lineNumberDigitCount).joinToString(separator = "") { "9" } |
||||
} |
||||
|
||||
Box(Modifier.fillMaxSize()) { |
||||
val scrollState = rememberLazyListState() |
||||
|
||||
LazyColumn( |
||||
modifier = Modifier.fillMaxSize(), |
||||
state = scrollState |
||||
) { |
||||
items(lines.size) { index -> |
||||
Box(Modifier.height(settings.fontSize.toDp() * 1.6f)) { |
||||
Line(Modifier.align(Alignment.CenterStart), maxNum, lines[index], settings) |
||||
} |
||||
} |
||||
} |
||||
|
||||
VerticalScrollbar( |
||||
Modifier.align(Alignment.CenterEnd), |
||||
scrollState |
||||
) |
||||
} |
||||
} |
||||
|
||||
// Поддержка русского языка |
||||
// دعم اللغة العربية |
||||
// 中文支持 |
||||
@Composable |
||||
private fun Line(modifier: Modifier, maxNum: String, line: Editor.Line, settings: Settings) { |
||||
Row(modifier = modifier) { |
||||
DisableSelection { |
||||
Box { |
||||
LineNumber(maxNum, Modifier.alpha(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 = LocalContentColor.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 { |
||||
buildAnnotatedString { |
||||
withStyle(AppTheme.code.simple) { |
||||
append(content.value.value) |
||||
} |
||||
} |
||||
}, |
||||
fontSize = settings.fontSize, |
||||
fontFamily = Fonts.jetbrainsMono(), |
||||
modifier = modifier, |
||||
softWrap = false |
||||
) |
||||
|
||||
private fun codeString(str: String) = buildAnnotatedString { |
||||
withStyle(AppTheme.code.simple) { |
||||
val strFormatted = str.replace("\t", " ") |
||||
append(strFormatted) |
||||
addStyle(AppTheme.code.punctuation, strFormatted, ":") |
||||
addStyle(AppTheme.code.punctuation, strFormatted, "=") |
||||
addStyle(AppTheme.code.punctuation, strFormatted, "\"") |
||||
addStyle(AppTheme.code.punctuation, strFormatted, "[") |
||||
addStyle(AppTheme.code.punctuation, strFormatted, "]") |
||||
addStyle(AppTheme.code.punctuation, strFormatted, "{") |
||||
addStyle(AppTheme.code.punctuation, strFormatted, "}") |
||||
addStyle(AppTheme.code.punctuation, strFormatted, "(") |
||||
addStyle(AppTheme.code.punctuation, strFormatted, ")") |
||||
addStyle(AppTheme.code.punctuation, strFormatted, ",") |
||||
addStyle(AppTheme.code.keyword, strFormatted, "fun ") |
||||
addStyle(AppTheme.code.keyword, strFormatted, "val ") |
||||
addStyle(AppTheme.code.keyword, strFormatted, "var ") |
||||
addStyle(AppTheme.code.keyword, strFormatted, "private ") |
||||
addStyle(AppTheme.code.keyword, strFormatted, "internal ") |
||||
addStyle(AppTheme.code.keyword, strFormatted, "for ") |
||||
addStyle(AppTheme.code.keyword, strFormatted, "expect ") |
||||
addStyle(AppTheme.code.keyword, strFormatted, "actual ") |
||||
addStyle(AppTheme.code.keyword, strFormatted, "import ") |
||||
addStyle(AppTheme.code.keyword, strFormatted, "package ") |
||||
addStyle(AppTheme.code.value, strFormatted, "true") |
||||
addStyle(AppTheme.code.value, strFormatted, "false") |
||||
addStyle(AppTheme.code.value, strFormatted, Regex("[0-9]*")) |
||||
addStyle(AppTheme.code.annotation, strFormatted, Regex("^@[a-zA-Z_]*")) |
||||
addStyle(AppTheme.code.comment, strFormatted, Regex("^\\s*//.*")) |
||||
} |
||||
} |
||||
|
||||
private fun AnnotatedString.Builder.addStyle(style: SpanStyle, text: String, regexp: String) { |
||||
addStyle(style, text, fromLiteral(regexp)) |
||||
} |
||||
|
||||
private fun AnnotatedString.Builder.addStyle(style: SpanStyle, text: String, regexp: Regex) { |
||||
for (result in regexp.findAll(text)) { |
||||
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(".").lowercase()) |
||||
} |
||||
|
||||
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,125 @@
|
||||
package org.jetbrains.codeviewer.ui.filetree |
||||
|
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.hoverable |
||||
import androidx.compose.foundation.interaction.MutableInteractionSource |
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState |
||||
import androidx.compose.foundation.layout.* |
||||
import androidx.compose.foundation.lazy.LazyColumn |
||||
import androidx.compose.foundation.lazy.rememberLazyListState |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.LocalContentColor |
||||
import androidx.compose.material.Surface |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.filled.* |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
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.LocalDensity |
||||
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.util.withoutWidthConstraints |
||||
|
||||
@Composable |
||||
fun FileTreeViewTabView() = Surface { |
||||
Row( |
||||
Modifier.padding(8.dp), |
||||
verticalAlignment = Alignment.CenterVertically |
||||
) { |
||||
Text( |
||||
"Files", |
||||
color = LocalContentColor.current.copy(alpha = 0.60f), |
||||
fontSize = 12.sp, |
||||
modifier = Modifier.padding(horizontal = 4.dp) |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun FileTreeView(model: FileTree) = Surface( |
||||
modifier = Modifier.fillMaxSize() |
||||
) { |
||||
with(LocalDensity.current) { |
||||
Box { |
||||
val scrollState = rememberLazyListState() |
||||
|
||||
LazyColumn( |
||||
modifier = Modifier.fillMaxSize().withoutWidthConstraints(), |
||||
state = scrollState |
||||
) { |
||||
items(model.items.size) { |
||||
FileTreeItemView(14.sp, 14.sp.toDp() * 1.5f, model.items[it]) |
||||
} |
||||
} |
||||
|
||||
VerticalScrollbar( |
||||
Modifier.align(Alignment.CenterEnd), |
||||
scrollState |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@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 interactionSource = remember { MutableInteractionSource() } |
||||
val active by interactionSource.collectIsHoveredAsState() |
||||
|
||||
FileItemIcon(Modifier.align(Alignment.CenterVertically), model) |
||||
Text( |
||||
text = model.name, |
||||
color = if (active) LocalContentColor.current.copy(alpha = 0.60f) else LocalContentColor.current, |
||||
modifier = Modifier |
||||
.align(Alignment.CenterVertically) |
||||
.clipToBounds() |
||||
.hoverable(interactionSource), |
||||
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, contentDescription = null, tint = LocalContentColor.current |
||||
) |
||||
else -> Icon( |
||||
Icons.Default.KeyboardArrowRight, contentDescription = null, tint = LocalContentColor.current |
||||
) |
||||
} |
||||
is FileTree.ItemType.File -> when (type.ext) { |
||||
"kt" -> Icon(Icons.Default.Code, contentDescription = null, tint = Color(0xFF3E86A0)) |
||||
"xml" -> Icon(Icons.Default.Code, contentDescription = null, tint = Color(0xFFC19C5F)) |
||||
"txt" -> Icon(Icons.Default.Description, contentDescription = null, tint = Color(0xFF87939A)) |
||||
"md" -> Icon(Icons.Default.Description, contentDescription = null, tint = Color(0xFF87939A)) |
||||
"gitignore" -> Icon(Icons.Default.BrokenImage, contentDescription = null, tint = Color(0xFF87939A)) |
||||
"gradle" -> Icon(Icons.Default.Build, contentDescription = null, tint = Color(0xFF87939A)) |
||||
"kts" -> Icon(Icons.Default.Build, contentDescription = null, tint = Color(0xFF3E86A0)) |
||||
"properties" -> Icon(Icons.Default.Settings, contentDescription = null, tint = Color(0xFF62B543)) |
||||
"bat" -> Icon(Icons.Default.Launch, contentDescription = null, tint = Color(0xFF87939A)) |
||||
else -> Icon(Icons.Default.TextSnippet, contentDescription = null, tint = Color(0xFF87939A)) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,47 @@
|
||||
package org.jetbrains.codeviewer.ui.statusbar |
||||
|
||||
import androidx.compose.foundation.layout.* |
||||
import androidx.compose.material.LocalContentColor |
||||
import androidx.compose.material.Slider |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.CompositionLocalProvider |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.platform.LocalDensity |
||||
import androidx.compose.ui.unit.* |
||||
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 = LocalContentColor.current.copy(alpha = 0.60f), |
||||
fontSize = 12.sp |
||||
) |
||||
|
||||
Spacer(Modifier.width(8.dp)) |
||||
|
||||
CompositionLocalProvider(LocalDensity provides LocalDensity.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) |
||||
private operator fun TextUnit.minus(other: TextUnit) = (value - other.value).sp |
||||
private operator fun TextUnit.div(other: TextUnit) = value / other.value |
@ -0,0 +1,11 @@
|
||||
package org.jetbrains.codeviewer.util |
||||
|
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.layout.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,27 @@
|
||||
package org.jetbrains.codeviewer.util |
||||
|
||||
import androidx.compose.runtime.* |
||||
import kotlinx.coroutines.CancellationException |
||||
import kotlinx.coroutines.CoroutineScope |
||||
|
||||
@Composable |
||||
fun <T : Any> loadable(load: () -> T): MutableState<T?> { |
||||
return loadableScoped { load() } |
||||
} |
||||
|
||||
private val loadingKey = Any() |
||||
|
||||
@Composable |
||||
fun <T : Any> loadableScoped(load: CoroutineScope.() -> T): MutableState<T?> { |
||||
val state: MutableState<T?> = remember { mutableStateOf(null) } |
||||
LaunchedEffect(loadingKey) { |
||||
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,13 @@
|
||||
package org.jetbrains.codeviewer.util |
||||
|
||||
interface TextLines { |
||||
val size: Int |
||||
fun get(index: Int): String |
||||
} |
||||
|
||||
object EmptyTextLines : TextLines { |
||||
override val size: Int |
||||
get() = 0 |
||||
|
||||
override fun get(index: Int): String = "" |
||||
} |
@ -0,0 +1,95 @@
|
||||
package org.jetbrains.codeviewer.util |
||||
|
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.gestures.Orientation |
||||
import androidx.compose.foundation.gestures.draggable |
||||
import androidx.compose.foundation.gestures.rememberDraggableState |
||||
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.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.layout.Layout |
||||
import androidx.compose.ui.platform.LocalDensity |
||||
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 VerticalSplittable( |
||||
modifier: Modifier, |
||||
splitterState: SplitterState, |
||||
onResize: (delta: Dp) -> Unit, |
||||
children: @Composable () -> Unit |
||||
) = Layout({ |
||||
children() |
||||
VerticalSplitter(splitterState, onResize) |
||||
}, modifier, measurePolicy = { 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 { |
||||
val density = LocalDensity.current |
||||
Box( |
||||
Modifier |
||||
.width(8.dp) |
||||
.fillMaxHeight() |
||||
.run { |
||||
if (splitterState.isResizeEnabled) { |
||||
this.draggable( |
||||
state = rememberDraggableState { |
||||
with(density) { |
||||
onResize(it.toDp()) |
||||
} |
||||
}, |
||||
orientation = Orientation.Horizontal, |
||||
startDragImmediately = true, |
||||
onDragStarted = { splitterState.isResizing = true }, |
||||
onDragStopped = { splitterState.isResizing = false } |
||||
).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,10 @@
|
||||
package org.jetbrains.codeviewer.platform |
||||
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.input.pointer.PointerIcon |
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon |
||||
import java.awt.Cursor |
||||
|
||||
actual fun Modifier.cursorForHorizontalResize(): Modifier = |
||||
this.pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR))) |
@ -0,0 +1,10 @@
|
||||
package org.jetbrains.codeviewer.platform |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.text.font.Font |
||||
import androidx.compose.ui.text.font.FontStyle |
||||
import androidx.compose.ui.text.font.FontWeight |
||||
|
||||
@Composable |
||||
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font = |
||||
androidx.compose.ui.text.platform.Font("font/$res.ttf", weight, style) |
@ -0,0 +1,25 @@
|
||||
package org.jetbrains.codeviewer.platform |
||||
|
||||
import androidx.compose.foundation.ScrollState |
||||
import androidx.compose.foundation.lazy.LazyListState |
||||
import androidx.compose.foundation.rememberScrollbarAdapter |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
|
||||
@Composable |
||||
actual fun VerticalScrollbar( |
||||
modifier: Modifier, |
||||
scrollState: ScrollState |
||||
) = androidx.compose.foundation.VerticalScrollbar( |
||||
rememberScrollbarAdapter(scrollState), |
||||
modifier |
||||
) |
||||
|
||||
@Composable |
||||
actual fun VerticalScrollbar( |
||||
modifier: Modifier, |
||||
scrollState: LazyListState |
||||
) = androidx.compose.foundation.VerticalScrollbar( |
||||
rememberScrollbarAdapter(scrollState), |
||||
modifier |
||||
) |
@ -0,0 +1,145 @@
|
||||
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.FilenameFilter |
||||
import java.io.IOException |
||||
import java.io.RandomAccessFile |
||||
import java.nio.channels.FileChannel |
||||
import java.nio.charset.StandardCharsets |
||||
|
||||
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(FilenameFilter { _, name -> !name.startsWith(".")}) |
||||
.orEmpty() |
||||
.map { it.toProjectFile() } |
||||
|
||||
override val hasChildren: Boolean |
||||
get() = isDirectory && listFiles()?.size ?: 0 > 0 |
||||
|
||||
|
||||
override fun readLines(scope: CoroutineScope): TextLines { |
||||
var byteBufferSize: Int |
||||
val byteBuffer = RandomAccessFile(this@toProjectFile, "r").use { file -> |
||||
byteBufferSize = file.length().toInt() |
||||
file.channel |
||||
.map(FileChannel.MapMode.READ_ONLY, 0, file.length()) |
||||
} |
||||
|
||||
val lineStartPositions = IntList() |
||||
|
||||
var size by mutableStateOf(0) |
||||
|
||||
val refreshJob = scope.launch { |
||||
delay(100) |
||||
size = lineStartPositions.size |
||||
while (true) { |
||||
delay(1000) |
||||
size = lineStartPositions.size |
||||
} |
||||
} |
||||
|
||||
scope.launch(Dispatchers.IO) { |
||||
readLinePositions(lineStartPositions) |
||||
refreshJob.cancel() |
||||
size = lineStartPositions.size |
||||
} |
||||
|
||||
return object : TextLines { |
||||
override val size get() = size |
||||
|
||||
override fun get(index: Int): String { |
||||
val startPosition = lineStartPositions[index] |
||||
val length = if (index + 1 < size) lineStartPositions[index + 1] - startPosition else |
||||
byteBufferSize - startPosition |
||||
// Only JDK since 13 has slice() method we need, so do ugly for now. |
||||
byteBuffer.position(startPosition) |
||||
val slice = byteBuffer.slice() |
||||
slice.limit(length) |
||||
return StandardCharsets.UTF_8.decode(slice).toString() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun java.io.File.readLinePositions( |
||||
starts: IntList |
||||
) { |
||||
require(length() <= Int.MAX_VALUE) { |
||||
"Files with size over ${Int.MAX_VALUE} aren't supported" |
||||
} |
||||
|
||||
val averageLineLength = 200 |
||||
starts.clear(length().toInt() / averageLineLength) |
||||
|
||||
try { |
||||
FileInputStream(this@readLinePositions).use { |
||||
val channel = it.channel |
||||
val ib = channel.map( |
||||
FileChannel.MapMode.READ_ONLY, 0, channel.size() |
||||
) |
||||
var isBeginOfLine = true |
||||
var position = 0L |
||||
while (ib.hasRemaining()) { |
||||
val byte = ib.get() |
||||
if (isBeginOfLine) { |
||||
starts.add(position.toInt()) |
||||
} |
||||
isBeginOfLine = byte.toInt().toChar() == '\n' |
||||
position++ |
||||
} |
||||
channel.close() |
||||
} |
||||
} catch (e: IOException) { |
||||
e.printStackTrace() |
||||
starts.clear(1) |
||||
starts.add(0) |
||||
} |
||||
|
||||
starts.compact() |
||||
} |
||||
|
||||
/** |
||||
* Compact version of List<Int> (without unboxing Int and using IntArray under the hood) |
||||
*/ |
||||
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,41 @@
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat |
||||
|
||||
plugins { |
||||
kotlin("multiplatform") // kotlin("jvm") doesn't work well in IDEA/AndroidStudio (https://github.com/JetBrains/compose-jb/issues/22) |
||||
id("org.jetbrains.compose") |
||||
} |
||||
|
||||
kotlin { |
||||
jvm {} |
||||
sourceSets { |
||||
named("jvmMain") { |
||||
dependencies { |
||||
implementation(compose.desktop.currentOs) |
||||
implementation(project(":common")) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
compose.desktop { |
||||
application { |
||||
mainClass = "org.jetbrains.codeviewer.MainKt" |
||||
|
||||
nativeDistributions { |
||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) |
||||
packageName = "ComposeCodeViewer" |
||||
packageVersion = "1.0.0" |
||||
|
||||
windows { |
||||
menu = true |
||||
// see https://wixtoolset.org/documentation/manual/v3/howtos/general/generate_guids.html |
||||
upgradeUuid = "AF792DA6-2EA3-495A-95E5-C3C6CBCB9948" |
||||
} |
||||
|
||||
macOS { |
||||
// Use -Pcompose.desktop.mac.sign=true to sign and notarize. |
||||
bundleID = "com.jetbrains.compose.codeviewer" |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,17 @@
|
||||
package org.jetbrains.codeviewer |
||||
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter |
||||
import androidx.compose.ui.res.loadImageBitmap |
||||
import androidx.compose.ui.res.useResource |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.compose.ui.window.WindowState |
||||
import androidx.compose.ui.window.singleWindowApplication |
||||
import org.jetbrains.codeviewer.ui.MainView |
||||
|
||||
fun main() = singleWindowApplication( |
||||
title = "Code Viewer", |
||||
state = WindowState(width = 1280.dp, height = 768.dp), |
||||
icon = BitmapPainter(useResource("ic_launcher.png", ::loadImageBitmap)), |
||||
) { |
||||
MainView() |
||||
} |
After Width: | Height: | Size: 9.1 KiB |
@ -0,0 +1,24 @@
|
||||
# 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 |
||||
kotlin.version=1.7.20 |
||||
agp.version=7.1.3 |
||||
compose.version=1.2.1 |
Binary file not shown.
@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME |
||||
distributionPath=wrapper/dists |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.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 |
After Width: | Height: | Size: 365 KiB |
After Width: | Height: | Size: 2.5 KiB |
@ -0,0 +1,23 @@
|
||||
pluginManagement { |
||||
repositories { |
||||
gradlePluginPortal() |
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") |
||||
google() |
||||
} |
||||
|
||||
plugins { |
||||
val kotlinVersion = extra["kotlin.version"] as String |
||||
val agpVersion = extra["agp.version"] as String |
||||
val composeVersion = extra["compose.version"] as String |
||||
|
||||
kotlin("jvm").version(kotlinVersion) |
||||
kotlin("multiplatform").version(kotlinVersion) |
||||
kotlin("android").version(kotlinVersion) |
||||
id("com.android.base").version(agpVersion) |
||||
id("com.android.application").version(agpVersion) |
||||
id("com.android.library").version(agpVersion) |
||||
id("org.jetbrains.compose").version(composeVersion) |
||||
} |
||||
} |
||||
|
||||
include(":common", ":android", ":desktop") |
Loading…
Reference in new issue