diff --git a/CHANGELOG.md b/CHANGELOG.md index d05a72e..b4e4d18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ ## [0.10.0] ### Added -- Add toggleable line numbers to `CodeTextView` and `CodeEditText` -- Updated sample app to include a toggle for line numbers -- Added `textStyle` param to `CodeTextView` and `CodeEditText` for custom text styling +- toggleable line numbers to `CodeTextView` and `CodeEditText` +- line numbers toggle to sample app +- `textStyle` param to `CodeTextView` and `CodeEditText` for custom text styling +- `hasHorizontalScroll` to support text next to lines scrolling ## [0.9.0] diff --git a/README.md b/README.md index 979d39f..6bfede3 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ repositories { ``` ```shell -implementation("dev.snipme:kodeview:0.9.0") +implementation("dev.snipme:kodeview:0.10.0") ``` ## Features ✨ @@ -31,6 +31,7 @@ implementation("dev.snipme:kodeview:0.9.0") - Multiple syntax languages (Java, Swift, Kotlin, C, ...) - Themes - Text bolding (emphasis) +- Line numbers - Written in pure Kotlin, so available for many platforms 📱 💻 🖥️ ## Support ☕ @@ -57,7 +58,12 @@ fun MyApp() { MaterialTheme { Column { - CodeTextView(highlights = highlights.value) + CodeTextView( + highlights = highlights.value, + showLineNumbers = true, + hasVerticalScroll = true, + hasHorizontalScroll = true, + ) } } } @@ -83,6 +89,9 @@ fun MyApp() { Column { CodeEditText( highlights = highlights.value, + showLineNumbers = true, + hasVerticalScroll = true, + hasHorizontalScroll = true, onValueChange = { textValue -> highlights.value = highlights.value.getBuilder() .code(textValue) diff --git a/androidExample/build.gradle.kts b/androidExample/build.gradle.kts index 06405c8..dd6abfc 100644 --- a/androidExample/build.gradle.kts +++ b/androidExample/build.gradle.kts @@ -55,9 +55,9 @@ dependencies { implementation(libs.activity.compose) implementation(compose.runtime) implementation(compose.foundation) - implementation(compose.material) implementation(compose.material3) implementation(compose.ui) implementation(compose.materialIconsExtended) implementation(project(":kodeview")) + implementation(project(":exampleCommon")) } \ No newline at end of file diff --git a/androidExample/src/main/java/dev/snipme/androidexample/Dropdown.kt b/androidExample/src/main/java/dev/snipme/androidexample/Dropdown.kt deleted file mode 100644 index 7afbf6b..0000000 --- a/androidExample/src/main/java/dev/snipme/androidexample/Dropdown.kt +++ /dev/null @@ -1,58 +0,0 @@ -package dev.snipme.androidexample - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -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.draw.clip -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp - -@Composable -fun Dropdown( - options: List, - selected: Int, - modifier: Modifier = Modifier, - onSelect: (String) -> Unit, -) { - var selectedOption by remember { - mutableStateOf(options[selected]) - } - - var isExpanded by remember { mutableStateOf(false) } - - DropdownMenu( - expanded = isExpanded, - onDismissRequest = { isExpanded = false }, - ) { - options.forEach { option -> - DropdownMenuItem( - text = { Text(text = option) }, - onClick = { - selectedOption = option - onSelect(option) - isExpanded = !isExpanded - }, - ) - } - } - - Text( - text = selectedOption, - textAlign = TextAlign.Center, - modifier = modifier - .clip(RoundedCornerShape(16.dp)) - .fillMaxWidth() - .clickable { isExpanded = !isExpanded } - .padding(8.dp), - ) -} \ No newline at end of file diff --git a/androidExample/src/main/java/dev/snipme/androidexample/MainActivity.kt b/androidExample/src/main/java/dev/snipme/androidexample/MainActivity.kt index 754947d..dd14a2a 100644 --- a/androidExample/src/main/java/dev/snipme/androidexample/MainActivity.kt +++ b/androidExample/src/main/java/dev/snipme/androidexample/MainActivity.kt @@ -1,17 +1,26 @@ package dev.snipme.androidexample +import Dropdown +import LineNumberSwitcher +import ThemeSwitcher import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.Divider +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -20,10 +29,14 @@ import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -47,126 +60,130 @@ class MainActivity : AppCompatActivity() { } } -private val sampleCode = - """ - class Main { - public static void main(String[] args) { - int abcd = 100; - } - } - """.trimIndent() - @Composable fun App() { - val isDarkModeState = remember { mutableStateOf(false) } - val areLineNumbersEnabled = remember { mutableStateOf(true) } - val isDarkMode = isDarkModeState.value - val lineNumbersEnabled = areLineNumbersEnabled.value - - val highlightsState = remember { - mutableStateOf( - Highlights.Builder(code = sampleCode).build() - ) - } + var isDarkMode by remember { mutableStateOf(false) } + var lineNumbersEnabled by remember { mutableStateOf(true) } + var currentLanguage by remember { mutableStateOf(SyntaxLanguage.DEFAULT) } - val highlights = highlightsState.value + var highlights by remember { + mutableStateOf(Highlights.Builder(code = Samples.kotlin).build()) + } fun updateSyntaxTheme(theme: SyntaxTheme) { - highlightsState.value = highlights.getBuilder() - .theme(theme) - .build() + highlights = highlights.getBuilder().theme(theme).build() } fun updateSyntaxLanguage(language: SyntaxLanguage) { - highlightsState.value = highlights.getBuilder() + highlights = highlights.getBuilder() .language(language) + .code(Samples.getSampleCode(language)) .build() + currentLanguage = language } MaterialTheme(colorScheme = if (isDarkMode) darkColorScheme() else lightColorScheme()) { - Surface { + Surface( + modifier = Modifier + .fillMaxSize() + ) { Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), + modifier = Modifier.fillMaxSize().padding(16.dp), + horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.SpaceBetween + verticalArrangement = Arrangement.Top ) { - Spacer(Modifier.height(8.dp)) - ThemeSwitcher( isDarkMode, - modifier = Modifier.fillMaxWidth(), - ) { setToDarkMode -> - isDarkModeState.value = setToDarkMode - updateSyntaxTheme(highlights.getTheme().useDark(setToDarkMode)!!) + modifier = Modifier.fillMaxWidth() + ) { setToDark -> + isDarkMode = setToDark + updateSyntaxTheme(highlights.getTheme().useDark(setToDark)!!) } Spacer(Modifier.height(16.dp)) - LineNumberSwitcher(lineNumbersEnabled, modifier = Modifier.fillMaxWidth()) { lineNumberEnabled -> - areLineNumbersEnabled.value = lineNumberEnabled + LineNumberSwitcher( + lineNumbersEnabled, + modifier = Modifier.fillMaxWidth() + ) { enabled -> + lineNumbersEnabled = enabled } + Spacer(Modifier.height(16.dp)) + Text( - modifier = Modifier.fillMaxWidth(), text = "KodeView", fontSize = 18.sp, - textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.size(16.dp)) + Spacer(Modifier.height(16.dp)) - CodeTextView(highlights = highlights, showLineNumbers = lineNumbersEnabled) + key(currentLanguage) { + CodeTextView( + modifier = Modifier.weight(1f) + .verticalScroll(rememberScrollState()), + highlights = highlights.getBuilder() + .code(Samples.getSampleCode(highlights.getLanguage())).build(), + showLineNumbers = lineNumbersEnabled, + hasHorizontalScroll = true, + textStyle = MaterialTheme.typography.bodyLarge, + ) + } - Spacer(modifier = Modifier.size(16.dp)) + Spacer(Modifier.height(16.dp)) HorizontalDivider() - Spacer(modifier = Modifier.size(16.dp)) - - Text("Edit this...") - CodeEditText( - highlights = highlights, - onValueChange = { textValue -> - highlightsState.value = highlights.getBuilder() - .code(textValue) - .build() - }, - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent, - ), - showLineNumbers = lineNumbersEnabled, - ) + Spacer(Modifier.height(16.dp)) - Spacer(modifier = Modifier.size(16.dp)) + key(highlights.getLanguage()) { + CodeEditText( + modifier = Modifier.weight(1f) + .verticalScroll(rememberScrollState()), + highlights = highlights, + showLineNumbers = lineNumbersEnabled, + hasHorizontalScroll = true, + textStyle = MaterialTheme.typography.bodyLarge, + label = { Text("Edit code") }, + onValueChange = { newText -> + highlights = highlights.getBuilder().code(newText).build() + }, + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + ), + ) + } - Spacer(modifier = Modifier.weight(1f)) + Spacer(Modifier.height(16.dp)) Dropdown( options = SyntaxThemes.getNames(), - selected = SyntaxThemes.themes().keys.indexOf(highlights.getTheme().key), + selected = SyntaxThemes.themes().keys.indexOf(highlights.getTheme().key) ) { selectedThemeName -> updateSyntaxTheme( SyntaxThemes.themes(isDarkMode)[selectedThemeName.lowercase()]!! ) } - Spacer(modifier = Modifier.size(16.dp)) + Spacer(Modifier.height(16.dp)) Dropdown( options = SyntaxLanguage.getNames(), selected = SyntaxLanguage.getNames().indexOfFirst { it.equals(highlights.getLanguage().name, ignoreCase = true) - }) { selectedLanguage -> + } + ) { selectedLanguage -> updateSyntaxLanguage(SyntaxLanguage.getByName(selectedLanguage)!!) } } } } -} \ No newline at end of file +} diff --git a/androidExample/src/main/java/dev/snipme/androidexample/ThemeSwitcher.kt b/androidExample/src/main/java/dev/snipme/androidexample/ThemeSwitcher.kt deleted file mode 100644 index e453f24..0000000 --- a/androidExample/src/main/java/dev/snipme/androidexample/ThemeSwitcher.kt +++ /dev/null @@ -1,40 +0,0 @@ -package dev.snipme.androidexample - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.DarkMode -import androidx.compose.material.icons.outlined.LightMode -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun ThemeSwitcher( - isDarkMode: Boolean, - modifier: Modifier = Modifier, - onChange: (Boolean) -> Unit, -) { - Surface { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(Modifier.width(8.dp)) - Icon(Icons.Outlined.LightMode, contentDescription = "Light mode") - Spacer(Modifier.width(16.dp)) - Switch(checked = isDarkMode, onCheckedChange = onChange) - Spacer(Modifier.width(16.dp)) - Icon(Icons.Default.DarkMode, contentDescription = "Dark mode") - Spacer(Modifier.width(8.dp)) - } - } -} \ No newline at end of file diff --git a/desktopExample/build.gradle.kts b/desktopExample/build.gradle.kts index 3b39176..49318dd 100644 --- a/desktopExample/build.gradle.kts +++ b/desktopExample/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(compose.components.resources) implementation(compose.desktop.currentOs) implementation(project(":kodeview")) + implementation(project(":exampleCommon")) } compose.desktop { diff --git a/desktopExample/src/main/kotlin/Dropdown.kt b/desktopExample/src/main/kotlin/Dropdown.kt deleted file mode 100644 index 8a12574..0000000 --- a/desktopExample/src/main/kotlin/Dropdown.kt +++ /dev/null @@ -1,58 +0,0 @@ -package dev.snipme.desktopexample - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -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.draw.clip -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp - -@Composable -fun Dropdown( - options: List, - selected: Int, - modifier: Modifier = Modifier, - onSelect: (String) -> Unit, -) { - var selectedOption by remember { - mutableStateOf(options[selected]) - } - - var isExpanded by remember { mutableStateOf(false) } - - DropdownMenu( - expanded = isExpanded, - onDismissRequest = { isExpanded = false }, - ) { - options.forEach { option -> - DropdownMenuItem( - text = { Text(text = option) }, - onClick = { - selectedOption = option - onSelect(option) - isExpanded = !isExpanded - }, - ) - } - } - - Text( - text = selectedOption, - textAlign = TextAlign.Center, - modifier = modifier - .clip(RoundedCornerShape(16.dp)) - .fillMaxWidth() - .clickable { isExpanded = !isExpanded } - .padding(8.dp), - ) -} \ No newline at end of file diff --git a/desktopExample/src/main/kotlin/Main.kt b/desktopExample/src/main/kotlin/Main.kt index e7e757a..e424c4b 100644 --- a/desktopExample/src/main/kotlin/Main.kt +++ b/desktopExample/src/main/kotlin/Main.kt @@ -1,23 +1,14 @@ package dev.snipme.desktopexample -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.material3.VerticalDivider -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import Dropdown +import LineNumberSwitcher +import Samples +import ThemeSwitcher +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign @@ -34,64 +25,51 @@ import dev.snipme.highlights.model.SyntaxThemes.useDark import dev.snipme.kodeview.view.CodeTextView import dev.snipme.kodeview.view.material3.CodeEditText -private val sampleCode = - """ - class Main { - public static void main(String[] args) { - int abcd = 100; - } - } - """.trimIndent() - fun main() = application { - val isDarkModeState = remember { mutableStateOf(false) } - val isDarkMode = isDarkModeState.value + var isDarkMode by remember { mutableStateOf(false) } + var lineNumbersEnabled by remember { mutableStateOf(false) } + var currentLanguage by remember { mutableStateOf(SyntaxLanguage.DEFAULT) } - val highlightsState = remember { + var highlights by remember { mutableStateOf( - Highlights.Builder(code = sampleCode).build() + Highlights.Builder(code = Samples.kotlin).build() ) } - val highlights = highlightsState.value fun updateSyntaxTheme(theme: SyntaxTheme) { - highlightsState.value = highlights.getBuilder() - .theme(theme) - .build() - } - - fun updateSyntaxLanguage(language: SyntaxLanguage) { - highlightsState.value = highlights.getBuilder() - .language(language) - .build() + highlights = highlights.getBuilder().theme(theme).build() } MaterialTheme(colorScheme = if (isDarkMode) darkColorScheme() else lightColorScheme()) { Window( onCloseRequest = ::exitApplication, - title = "KodeView example", + title = "KodeView Example", state = rememberWindowState() ) { Surface { Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), + modifier = Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.Center, ) { Spacer(Modifier.height(8.dp)) ThemeSwitcher( isDarkMode, - modifier = Modifier.fillMaxWidth(), - ) { setToDarkMode -> - isDarkModeState.value = setToDarkMode - updateSyntaxTheme(highlights.getTheme().useDark(setToDarkMode)!!) + modifier = Modifier.fillMaxWidth() + ) { dark -> + isDarkMode = dark + updateSyntaxTheme(highlights.getTheme().useDark(dark)!!) } - Spacer(Modifier.height(16.dp)) + LineNumberSwitcher( + lineNumbersEnabled, + modifier = Modifier.fillMaxWidth() + ) { enabled -> + lineNumbersEnabled = enabled + } + Text( modifier = Modifier.fillMaxWidth(), text = "KodeView", @@ -99,57 +77,74 @@ fun main() = application { textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.size(16.dp)) + Spacer(Modifier.size(16.dp)) Row( - modifier = Modifier - .weight(1f) - .fillMaxWidth(), + modifier = Modifier.weight(1f).fillMaxWidth() ) { - CodeTextView( - modifier = Modifier.weight(1f), - highlights = highlights, - ) + key(currentLanguage) { + CodeTextView( + modifier = Modifier.weight(1f) + .verticalScroll(rememberScrollState()), + highlights = highlights.getBuilder() + .code(Samples.getSampleCode(highlights.getLanguage())).build(), + showLineNumbers = lineNumbersEnabled, + hasHorizontalScroll = true, + textStyle = MaterialTheme.typography.bodyMedium, + ) + } + VerticalDivider(Modifier.padding(8.dp)) - CodeEditText( - modifier = Modifier.weight(1f), - label = { Text("Edit code") }, - highlights = highlights, - onValueChange = { textValue -> - highlightsState.value = highlights.getBuilder() - .code(textValue) - .build() - }, - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent, - ), - ) + key(highlights.getLanguage()) { + CodeEditText( + modifier = Modifier.weight(1f) + .verticalScroll(rememberScrollState()), + highlights = highlights, + showLineNumbers = lineNumbersEnabled, + hasHorizontalScroll = true, + textStyle = MaterialTheme.typography.bodyMedium, + label = { Text("Edit code") }, + onValueChange = { newText -> + highlights = highlights.getBuilder().code(newText).build() + }, + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + ), + ) + } } - Spacer(modifier = Modifier.size(16.dp)) + Spacer(Modifier.size(16.dp)) Dropdown( options = SyntaxThemes.getNames(), - selected = SyntaxThemes.themes().keys.indexOf(highlights.getTheme().key), + selected = SyntaxThemes.themes().keys.indexOf(highlights.getTheme().key) ) { selectedThemeName -> updateSyntaxTheme( SyntaxThemes.themes(isDarkMode)[selectedThemeName.lowercase()]!! ) } - Spacer(modifier = Modifier.size(16.dp)) + Spacer(Modifier.size(16.dp)) Dropdown( options = SyntaxLanguage.getNames(), selected = SyntaxLanguage.getNames().indexOfFirst { it.equals(highlights.getLanguage().name, ignoreCase = true) - }) { selectedLanguage -> - updateSyntaxLanguage(SyntaxLanguage.getByName(selectedLanguage)!!) + } + ) { selectedLanguage -> + val language = SyntaxLanguage.getByName(selectedLanguage)!! + if (language == highlights.getLanguage()) return@Dropdown + currentLanguage = language + highlights = highlights.getBuilder() + .language(language) + .code(Samples.getSampleCode(language)) + .build() } } } diff --git a/desktopExample/src/main/kotlin/ThemeSwitcher.kt b/desktopExample/src/main/kotlin/ThemeSwitcher.kt deleted file mode 100644 index f44c69f..0000000 --- a/desktopExample/src/main/kotlin/ThemeSwitcher.kt +++ /dev/null @@ -1,40 +0,0 @@ -package dev.snipme.desktopexample - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.DarkMode -import androidx.compose.material.icons.outlined.LightMode -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun ThemeSwitcher( - isDarkMode: Boolean, - modifier: Modifier = Modifier, - onChange: (Boolean) -> Unit, -) { - Surface { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(Modifier.width(8.dp)) - Icon(Icons.Outlined.LightMode, contentDescription = "Light mode") - Spacer(Modifier.width(16.dp)) - Switch(checked = isDarkMode, onCheckedChange = onChange) - Spacer(Modifier.width(16.dp)) - Icon(Icons.Default.DarkMode, contentDescription = "Dark mode") - Spacer(Modifier.width(8.dp)) - } - } -} \ No newline at end of file diff --git a/exampleCommon/build.gradle.kts b/exampleCommon/build.gradle.kts new file mode 100644 index 0000000..47c0164 --- /dev/null +++ b/exampleCommon/build.gradle.kts @@ -0,0 +1,51 @@ +@Suppress("DSL_SCOPE_VIOLATION") plugins { + alias(libs.plugins.multiplatform) + alias(libs.plugins.compose) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.android.library) +} + +kotlin { + androidTarget() + jvm() + js(IR) { + browser() + binaries.executable() + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.ui) + implementation(compose.materialIconsExtended) + implementation(project(":kodeview")) + } + } + + val androidMain by getting + + val jvmMain by getting + val jsMain by getting + } +} + +android { + namespace = "dev.snipme.examplecommon" + compileSdk = 34 + + defaultConfig { + minSdk = 21 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlin { + jvmToolchain(17) // Ensure Kotlin matches Java version + } +} diff --git a/webExample/src/jsMain/kotlin/Dropdown.kt b/exampleCommon/src/commonMain/kotlin/Dropdown.kt similarity index 100% rename from webExample/src/jsMain/kotlin/Dropdown.kt rename to exampleCommon/src/commonMain/kotlin/Dropdown.kt diff --git a/androidExample/src/main/java/dev/snipme/androidexample/LineNumberSwitcher.kt b/exampleCommon/src/commonMain/kotlin/LineNumberSwitcher.kt similarity index 79% rename from androidExample/src/main/java/dev/snipme/androidexample/LineNumberSwitcher.kt rename to exampleCommon/src/commonMain/kotlin/LineNumberSwitcher.kt index 207a0cc..4486c2b 100644 --- a/androidExample/src/main/java/dev/snipme/androidexample/LineNumberSwitcher.kt +++ b/exampleCommon/src/commonMain/kotlin/LineNumberSwitcher.kt @@ -1,20 +1,14 @@ -package dev.snipme.androidexample - import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.FormatAlignLeft -import androidx.compose.material.icons.filled.DarkMode -import androidx.compose.material.icons.outlined.FormatAlignLeft import androidx.compose.material.icons.outlined.FormatListNumbered -import androidx.compose.material.icons.outlined.LightMode import androidx.compose.material.icons.outlined.Reorder +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -33,11 +27,11 @@ fun LineNumberSwitcher( verticalAlignment = Alignment.CenterVertically, ) { Spacer(Modifier.width(8.dp)) - Icon(Icons.Outlined.FormatListNumbered, contentDescription = "Line numbers enabled") + Icon(Icons.Outlined.Reorder, contentDescription = "Line numbers disabled") Spacer(Modifier.width(16.dp)) Switch(checked = lineNumbersEnabled, onCheckedChange = onChange) Spacer(Modifier.width(16.dp)) - Icon(Icons.Outlined.Reorder, contentDescription = "Dark mode") + Icon(Icons.Outlined.FormatListNumbered, contentDescription = "Line numbers enabled") Spacer(Modifier.width(8.dp)) } } diff --git a/exampleCommon/src/commonMain/kotlin/Samples.kt b/exampleCommon/src/commonMain/kotlin/Samples.kt new file mode 100644 index 0000000..11dec42 --- /dev/null +++ b/exampleCommon/src/commonMain/kotlin/Samples.kt @@ -0,0 +1,157 @@ +import dev.snipme.highlights.model.SyntaxLanguage + +class Samples { + companion object { + val c = """ + #include + + int main() { + int abcd = 100; + printf("Value: %d\n", abcd); + return 0; + } + """.trimIndent() + + val cpp = """ + #include + using namespace std; + + int main() { + int abcd = 100; + cout << "Value: " << abcd << endl; + return 0; + } + """.trimIndent() + + val dart = """ + void main() { + int abcd = 100; + print("Value: ${'$'}abcd"); + } + """.trimIndent() + + val java = """ + class Main { + public static void main(String[] args) { + int abcd = 100; + System.out.println("Value: " + abcd); + } + } + """.trimIndent() + + val javaStarter = """ + class Main { + public static void main(String[] args) { + int abcd = 100; + System.out.println("Value: " + abcd); + } + } + """.trimIndent() + + val kotlin = """ + fun main() { + val abcd = 100 + println("Value: ${'$'}abcd") + } + """.trimIndent() + + val rust = """ + fn main() { + let abcd: i32 = 100; + println!("Value: {}", abcd); + } + """.trimIndent() + + val csharp = """ + using System; + + class Program { + static void Main() { + int abcd = 100; + Console.WriteLine("Value: " + abcd); + } + } + """.trimIndent() + + val coffeescript = """ + abcd = 100 + console.log "Value: #{abcd}" + """.trimIndent() + + val javascript = """ + let abcd = 100; + console.log("Value: " + abcd); + """.trimIndent() + + val perl = """ + my \${'$'}abcd = 100; + print "Value: ${'$'}abcd\n"; + """.trimIndent() + + val python = """ + abcd = 100 + print("Value:", abcd) + """.trimIndent() + + val ruby = """ + abcd = 100 + puts "Value: #{abcd}" + """.trimIndent() + + val shell = """ + #!/bin/bash + abcd=100 + echo "Value: ${'$'}abcd" + """.trimIndent() + + val swift = """ + import Foundation + + let abcd = 100 + print("Value: \(abcd)") + """.trimIndent() + + val typescript = """ + let abcd: number = 100; + console.log("Value: " + abcd); + """.trimIndent() + + val go = """ + package main + import "fmt" + + func main() { + abcd := 100 + fmt.Println("Value:", abcd) + } + """.trimIndent() + + val php = """ + + """.trimIndent() + + fun getSampleCode(language: SyntaxLanguage): String = when (language) { + SyntaxLanguage.DEFAULT -> kotlin + SyntaxLanguage.C -> c + SyntaxLanguage.CPP -> cpp + SyntaxLanguage.DART -> dart + SyntaxLanguage.JAVA -> java + SyntaxLanguage.KOTLIN -> kotlin + SyntaxLanguage.RUST -> rust + SyntaxLanguage.CSHARP -> csharp + SyntaxLanguage.COFFEESCRIPT -> coffeescript + SyntaxLanguage.JAVASCRIPT -> javascript + SyntaxLanguage.PERL -> perl + SyntaxLanguage.PYTHON -> python + SyntaxLanguage.RUBY -> ruby + SyntaxLanguage.SHELL -> shell + SyntaxLanguage.SWIFT -> swift + SyntaxLanguage.TYPESCRIPT -> typescript + SyntaxLanguage.GO -> go + SyntaxLanguage.PHP -> php + } + } +} diff --git a/webExample/src/jsMain/kotlin/ThemeSwitcher.kt b/exampleCommon/src/commonMain/kotlin/ThemeSwitcher.kt similarity index 100% rename from webExample/src/jsMain/kotlin/ThemeSwitcher.kt rename to exampleCommon/src/commonMain/kotlin/ThemeSwitcher.kt diff --git a/kodeview/build.gradle.kts b/kodeview/build.gradle.kts index 2e20552..12d97c3 100644 --- a/kodeview/build.gradle.kts +++ b/kodeview/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.snipme" -version = "0.9.0" +version = "0.10.0" android { namespace = "dev.snipme.kodeview" diff --git a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeEditText.kt b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeEditText.kt index 62ad281..94e2ab4 100644 --- a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeEditText.kt +++ b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeEditText.kt @@ -1,35 +1,38 @@ package dev.snipme.kodeview.view import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.LocalTextStyle -import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.material.TextFieldColors import androidx.compose.material.TextFieldDefaults -import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf 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.graphics.Shape import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp import copySpanStyles import dev.snipme.highlights.DefaultHighlightsResultListener -import updateIndentations import dev.snipme.highlights.Highlights import dev.snipme.highlights.model.CodeHighlight import generateAnnotatedString -import androidx.compose.ui.unit.dp +import updateIndentations +// TODO Refactor and test @Composable fun CodeEditText( highlights: Highlights, @@ -54,6 +57,7 @@ fun CodeEditText( shape: Shape = TextFieldDefaults.TextFieldShape, colors: TextFieldColors = TextFieldDefaults.textFieldColors(), showLineNumbers: Boolean = false, + hasHorizontalScroll: Boolean = false, lineNumberTextStyle: TextStyle = textStyle.copy() ) { val currentText = remember { @@ -88,9 +92,9 @@ fun CodeEditText( Row(modifier = modifier) { if (showLineNumbers) { val lines = currentText.value.text.lines().size - Column(horizontalAlignment = Alignment.End,) { + Column(horizontalAlignment = Alignment.End) { for (i in 1..lines) { - androidx.compose.material.Text( + Text( text = i.toString(), style = lineNumberTextStyle ) diff --git a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeTextView.kt b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeTextView.kt index 34ac50d..3da5295 100644 --- a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeTextView.kt +++ b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeTextView.kt @@ -1,71 +1,38 @@ package dev.snipme.kodeview.view import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp import dev.snipme.highlights.Highlights -import generateAnnotatedString @Composable fun CodeTextView( modifier: Modifier = Modifier.background(Color.Transparent), highlights: Highlights, textStyle: TextStyle = LocalTextStyle.current, + hasHorizontalScroll: Boolean = false, showLineNumbers: Boolean = false, - lineNumberTextStyle: TextStyle = textStyle.copy() + lineNumberTextStyle: TextStyle = textStyle.copy(), ) { - var textState by remember { - mutableStateOf(AnnotatedString(highlights.getCode())) - } - - LaunchedEffect(highlights) { - textState = highlights - .getHighlights() - .generateAnnotatedString(highlights.getCode()) - } + val (textState, _) = rememberTextStateWithHighlights(highlights) Surface( modifier = modifier, color = Color.Transparent ) { - Row(modifier = Modifier - .verticalScroll(rememberScrollState()) - .horizontalScroll(rememberScrollState()) + LineNumberWrapper( + text = textState.text, + hasHorizontalScroll = hasHorizontalScroll, + showLineNumbers = showLineNumbers, + lineNumberTextStyle = lineNumberTextStyle ) { - if (showLineNumbers) { - val lines = textState.text.lines().size - Column(horizontalAlignment = Alignment.End) { - for (i in 1..lines) { - Text( - text = i.toString(), - style = lineNumberTextStyle, - ) - } - } - Spacer(modifier = Modifier.width(8.dp)) - } Text( - text = textState, + text = textState.annotatedString, style = textStyle ) } diff --git a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/Common.kt b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/Common.kt new file mode 100644 index 0000000..91b7fe7 --- /dev/null +++ b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/Common.kt @@ -0,0 +1,121 @@ +package dev.snipme.kodeview.view + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import copySpanStyles +import dev.snipme.highlights.DefaultHighlightsResultListener +import dev.snipme.highlights.Highlights +import dev.snipme.highlights.model.CodeHighlight +import generateAnnotatedString +import updateIndentations + +@Composable +internal fun rememberTextStateWithHighlights( + highlights: Highlights, + onValueChange: (String) -> Unit = {}, + handleIndentations: Boolean = true, + async: Boolean = true, +): Pair Unit> { + val textState = remember { + mutableStateOf( + TextFieldValue( + AnnotatedString(highlights.getCode()) + ) + ) + } + + val updateCallback: (TextFieldValue) -> Unit = { change -> + val updated = change.updateIndentations(handleIndentations) + if (updated.text != textState.value.text) { + onValueChange(updated.text) + } + + textState.value = updated.copySpanStyles(textState.value) + } + + LaunchedEffect(highlights) { + if (async) { + highlights.getHighlightsAsync(object : DefaultHighlightsResultListener() { + override fun onSuccess(result: List) { + textState.value = textState.value.copy( + annotatedString = result.generateAnnotatedString(textState.value.text), + ) + } + }) + return@LaunchedEffect + } + + val result = highlights.getHighlights() + textState.value = textState.value.copy( + annotatedString = result.generateAnnotatedString(textState.value.text), + ) + } + + + + return Pair(textState.value, updateCallback) +} + +@Composable +internal fun LineNumberWrapper( + modifier: Modifier = Modifier, + text: String, + hasHorizontalScroll: Boolean, + showLineNumbers: Boolean, + lineNumberTextStyle: androidx.compose.ui.text.TextStyle, + numbersPadding: PaddingValues = PaddingValues(), + content: @Composable () -> Unit +) { + val modifierWithScroll = modifier.then( + if (showLineNumbers || hasHorizontalScroll) { + Modifier.horizontalScroll(rememberScrollState()) + } else { + Modifier + } + ) + + Row(modifier = modifier.fillMaxWidth()) { + if (showLineNumbers) { + val lines = text.lines().size + Column( + modifier = Modifier.padding(numbersPadding), horizontalAlignment = Alignment.End + ) { + for (i in 1..lines) { + Text( + text = i.toString(), + style = lineNumberTextStyle.copy( + fontFamily = FontFamily.Monospace + ), + ) + } + } + Spacer(modifier = Modifier.width(8.dp)) + Box(modifier = modifierWithScroll) { + content() + } + } else { + Box(modifier = modifierWithScroll) { + content() + } + } + } +} \ No newline at end of file diff --git a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/material3/CodeEditText.kt b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/material3/CodeEditText.kt index 470525f..ee566f2 100644 --- a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/material3/CodeEditText.kt +++ b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/material3/CodeEditText.kt @@ -1,21 +1,20 @@ package dev.snipme.kodeview.view.material3 +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Divider +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf 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.graphics.Shape @@ -28,6 +27,8 @@ import copySpanStyles import dev.snipme.highlights.DefaultHighlightsResultListener import dev.snipme.highlights.Highlights import dev.snipme.highlights.model.CodeHighlight +import dev.snipme.kodeview.view.LineNumberWrapper +import dev.snipme.kodeview.view.rememberTextStateWithHighlights import generateAnnotatedString import updateIndentations import androidx.compose.material3.LocalTextStyle as LocalTextStyle3 @@ -60,80 +61,54 @@ fun CodeEditText( shape: Shape = TextFieldDefaults3.shape, colors: TextFieldColors3 = TextFieldDefaults3.colors(), showLineNumbers: Boolean = false, + hasHorizontalScroll: Boolean = false, lineNumberTextStyle: TextStyle = textStyle.copy() ) { - val currentText = remember { - mutableStateOf( - TextFieldValue( - AnnotatedString(highlights.getCode()) + val (currentText, updateCallback) = rememberTextStateWithHighlights( + highlights, + onValueChange = onValueChange, + handleIndentations = handleIndentations, + ) + + val labelPadding = label?.run { 8.dp } ?: 0.dp + val numbersPadding = PaddingValues(top = 16.dp + labelPadding) + + Surface( + modifier = modifier.fillMaxWidth(), + ) { + LineNumberWrapper( + text = currentText.text, + showLineNumbers = showLineNumbers, + hasHorizontalScroll = hasHorizontalScroll, + lineNumberTextStyle = lineNumberTextStyle, + numbersPadding = numbersPadding, + ) { + TextField3( + onValueChange = updateCallback, + value = currentText, + enabled = enabled, + readOnly = readOnly, + textStyle = textStyle, + label = label, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + isError = isError, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + interactionSource = interactionSource, + shape = shape, + colors = colors, ) - ) - } - - - LaunchedEffect(highlights) { - highlights.getHighlightsAsync(object : DefaultHighlightsResultListener() { - override fun onSuccess(result: List) { - currentText.value = currentText.value.copy( - annotatedString = result.generateAnnotatedString(currentText.value.text), - ) - } - }) - } - - fun updateNewValue(change: TextFieldValue) { - val updated = change.updateIndentations(handleIndentations) - if (updated.text != currentText.value.text) { - onValueChange(updated.text) - } - - currentText.value = updated.copySpanStyles( - currentText.value - ) - } - - Row(modifier = modifier.fillMaxWidth()) { - if (showLineNumbers) { - val lines = currentText.value.text.lines().size.coerceAtLeast(minLines) - Column( - horizontalAlignment = Alignment.End, - modifier = Modifier - .padding(top = 16.dp, end = 8.dp) - ) { - (1..lines).forEach { i -> - Text( - text = i.toString(), - style = lineNumberTextStyle, - ) - } - } } - - TextField3( - modifier = Modifier.weight(1f), - onValueChange = ::updateNewValue, - value = currentText.value, - enabled = enabled, - readOnly = readOnly, - textStyle = textStyle, - label = label, - placeholder = placeholder, - leadingIcon = leadingIcon, - trailingIcon = trailingIcon, - isError = isError, - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - singleLine = singleLine, - maxLines = maxLines, - minLines = minLines, - interactionSource = interactionSource, - shape = shape, - colors = colors, - ) } } +// TODO Refactor and test @Composable fun CodeEditTextSwiftUi( highlights: Highlights, @@ -200,8 +175,7 @@ fun CodeEditTextSwiftUi( if (showLineNumbers) { val lines = currentText.value.text.lines().size.coerceAtLeast(minLines) Column( - modifier = Modifier - .padding(top = 16.dp, end = 8.dp) // Align with TextField's internal padding + modifier ) { (1..lines).forEach { i -> Text( @@ -212,8 +186,15 @@ fun CodeEditTextSwiftUi( } } + // TODO Extract common logic to a wrapper + + val lineNumberAwareModifier = if (showLineNumbers) + modifier.horizontalScroll(rememberScrollState()) + else + modifier + TextField3( - modifier = Modifier.weight(1f), + modifier = lineNumberAwareModifier.weight(1f), value = currentText.value, onValueChange = ::updateNewValue, enabled = enabled, diff --git a/settings.gradle.kts b/settings.gradle.kts index a509564..8e0d666 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,4 +43,5 @@ rootProject.name = "KodeView" include(":kodeview") include(":androidExample") include(":desktopExample") -include(":webExample") \ No newline at end of file +include(":webExample") +include(":exampleCommon") \ No newline at end of file diff --git a/webExample/build.gradle.kts b/webExample/build.gradle.kts index 7606433..7c850d8 100644 --- a/webExample/build.gradle.kts +++ b/webExample/build.gradle.kts @@ -20,6 +20,7 @@ kotlin { implementation(compose.material3) implementation(compose.materialIconsExtended) implementation(project(":kodeview")) + implementation(project(":exampleCommon")) } } } diff --git a/webExample/src/jsMain/kotlin/main.js.kt b/webExample/src/jsMain/kotlin/main.js.kt index 725b20c..5d89d48 100644 --- a/webExample/src/jsMain/kotlin/main.js.kt +++ b/webExample/src/jsMain/kotlin/main.js.kt @@ -1,20 +1,27 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Divider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.VerticalDivider import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -28,19 +35,10 @@ import dev.snipme.highlights.model.SyntaxLanguage import dev.snipme.highlights.model.SyntaxTheme import dev.snipme.highlights.model.SyntaxThemes import dev.snipme.highlights.model.SyntaxThemes.useDark -import dev.snipme.kodeview.view.material3.CodeEditText import dev.snipme.kodeview.view.CodeTextView +import dev.snipme.kodeview.view.material3.CodeEditText import org.jetbrains.skiko.wasm.onWasmReady -private val sampleCode = - """ - class Main { - public static void main(String[] args) { - int abcd = 100; - } - } - """.trimIndent() - @ExperimentalComposeUiApi fun main() { onWasmReady { @@ -49,49 +47,45 @@ fun main() { canvasElementId = "ComposeTarget", applyDefaultStyles = true, ) { - val isDarkModeState = remember { mutableStateOf(false) } - val isDarkMode = isDarkModeState.value + var isDarkMode by remember { mutableStateOf(false) } + var lineNumbersEnabled by remember { mutableStateOf(false) } + var currentLanguage by remember { mutableStateOf(SyntaxLanguage.DEFAULT) } - val highlightsState = remember { + var highlights by remember { mutableStateOf( - Highlights.Builder(code = sampleCode).build() + Highlights.Builder(code = Samples.kotlin).build() ) } - val highlights = highlightsState.value fun updateSyntaxTheme(theme: SyntaxTheme) { - highlightsState.value = highlights.getBuilder() - .theme(theme) - .build() - } - - fun updateSyntaxLanguage(language: SyntaxLanguage) { - highlightsState.value = highlights.getBuilder() - .language(language) - .build() + highlights = highlights.getBuilder().theme(theme).build() } MaterialTheme(colorScheme = if (isDarkMode) darkColorScheme() else lightColorScheme()) { Surface { Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.SpaceBetween + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center, ) { Spacer(Modifier.height(8.dp)) ThemeSwitcher( isDarkMode, - modifier = Modifier.fillMaxWidth(), - ) { setToDarkMode -> - isDarkModeState.value = setToDarkMode - updateSyntaxTheme(highlights.getTheme().useDark(setToDarkMode)!!) + modifier = Modifier.fillMaxWidth() + ) { dark -> + isDarkMode = dark + updateSyntaxTheme(highlights.getTheme().useDark(dark)!!) } Spacer(Modifier.height(16.dp)) + LineNumberSwitcher( + lineNumbersEnabled, + modifier = Modifier.fillMaxWidth() + ) { enabled -> + lineNumbersEnabled = enabled + } + Text( modifier = Modifier.fillMaxWidth(), text = "KodeView", @@ -99,55 +93,75 @@ fun main() { textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.size(16.dp)) - - CodeTextView(highlights = highlights) - - Spacer(modifier = Modifier.size(16.dp)) - - Divider() - - Spacer(modifier = Modifier.size(16.dp)) - - Text("Edit this...") - CodeEditText( - highlights = highlights, - onValueChange = { textValue -> - highlightsState.value = highlights.getBuilder() - .code(textValue) - .build() - }, - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent, - ), - ) - - Spacer(modifier = Modifier.size(16.dp)) + Spacer(Modifier.size(16.dp)) + + Row( + modifier = Modifier.weight(1f).fillMaxWidth() + ) { + key(currentLanguage) { + CodeTextView( + modifier = Modifier.weight(1f) + .verticalScroll(rememberScrollState()), + highlights = highlights.getBuilder() + .code(Samples.getSampleCode(highlights.getLanguage())) + .build(), + showLineNumbers = lineNumbersEnabled, + hasHorizontalScroll = true, + textStyle = MaterialTheme.typography.bodyMedium, + ) + } + + VerticalDivider(Modifier.padding(8.dp)) + key(highlights.getLanguage()) { + CodeEditText( + modifier = Modifier.weight(1f) + .verticalScroll(rememberScrollState()), + highlights = highlights, + showLineNumbers = lineNumbersEnabled, + hasHorizontalScroll = true, + textStyle = MaterialTheme.typography.bodyMedium, + label = { Text("Edit code") }, + onValueChange = { newText -> + highlights = highlights.getBuilder().code(newText).build() + }, + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + ), + ) + } + } - Spacer(modifier = Modifier.weight(1f)) + Spacer(Modifier.size(16.dp)) Dropdown( options = SyntaxThemes.getNames(), - selected = SyntaxThemes.themes().keys.indexOf(highlights.getTheme().key), + selected = SyntaxThemes.themes().keys.indexOf(highlights.getTheme().key) ) { selectedThemeName -> updateSyntaxTheme( SyntaxThemes.themes(isDarkMode)[selectedThemeName.lowercase()]!! ) } - Spacer(modifier = Modifier.size(16.dp)) + Spacer(Modifier.size(16.dp)) Dropdown( options = SyntaxLanguage.getNames(), selected = SyntaxLanguage.getNames().indexOfFirst { it.equals(highlights.getLanguage().name, ignoreCase = true) - }) { selectedLanguage -> - updateSyntaxLanguage(SyntaxLanguage.getByName(selectedLanguage)!!) + } + ) { selectedLanguage -> + val language = SyntaxLanguage.getByName(selectedLanguage)!! + if (language == highlights.getLanguage()) return@Dropdown + currentLanguage = language + highlights = highlights.getBuilder() + .language(language) + .code(Samples.getSampleCode(language)) + .build() } } }