diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts
new file mode 100644
index 0000000..3b1741a
--- /dev/null
+++ b/androidApp/build.gradle.kts
@@ -0,0 +1,47 @@
+plugins {
+ alias(libs.plugins.androidApplication)
+ alias(libs.plugins.composeCompiler)
+}
+
+android {
+ namespace = "dev.skymansandy.jsoncmpsample"
+ compileSdk {
+ version = release(36)
+ }
+
+ defaultConfig {
+ applicationId = "dev.skymansandy.jsoncmpsample"
+ minSdk = 24
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ buildFeatures {
+ compose = true
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.compose.ui)
+ implementation(libs.compose.material3)
+ implementation(libs.compose.uiToolingPreview)
+ debugImplementation(libs.compose.uiTooling)
+ implementation(project(":composeApp"))
+}
diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..386388d
--- /dev/null
+++ b/androidApp/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/androidApp/src/main/kotlin/dev/skymansandy/jsoncmpsample/JsonCmpSampleActivity.kt b/androidApp/src/main/kotlin/dev/skymansandy/jsoncmpsample/JsonCmpSampleActivity.kt
new file mode 100644
index 0000000..23ddbee
--- /dev/null
+++ b/androidApp/src/main/kotlin/dev/skymansandy/jsoncmpsample/JsonCmpSampleActivity.kt
@@ -0,0 +1,17 @@
+package dev.skymansandy.jsoncmpsample
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import dev.skymansandy.jsoncmpsample.ui.App
+
+internal class JsonCmpSampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ App()
+ }
+ }
+}
diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2bf4624
--- /dev/null
+++ b/androidApp/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ JsonCMP Sample
+
diff --git a/androidApp/src/main/res/values/themes.xml b/androidApp/src/main/res/values/themes.xml
new file mode 100644
index 0000000..638cd9a
--- /dev/null
+++ b/androidApp/src/main/res/values/themes.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/composeApp/src/commonMain/kotlin/dev/skymansandy/jsoncmpsample/App.kt b/composeApp/src/commonMain/kotlin/dev/skymansandy/jsoncmpsample/App.kt
deleted file mode 100644
index 5cafc61..0000000
--- a/composeApp/src/commonMain/kotlin/dev/skymansandy/jsoncmpsample/App.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package dev.skymansandy.jsoncmpsample
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.FilterChip
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Text
-import androidx.compose.material3.darkColorScheme
-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.unit.dp
-import dev.skymansandy.jsoncmp.JsonCMP
-import dev.skymansandy.jsoncmp.config.rememberJsonEditorState
-import dev.skymansandy.jsoncmp.helper.constants.colors.JsonCmpColors
-
-private val sampleJson = """
-{
- "name": "JsonCMP",
- "version": "1.0.0",
- "description": "Kotlin Multiplatform Compose JSON viewer and editor",
- "platforms": ["Android", "iOS", "JVM Desktop"],
- "features": {
- "viewer": true,
- "editor": true,
- "search": true,
- "folding": true,
- "sorting": true
- },
- "themes": [
- {"name": "Dark", "type": "dark"},
- {"name": "Light", "type": "light"},
- {"name": "Monokai", "type": "dark"},
- {"name": "Dracula", "type": "dark"},
- {"name": "Solarized Dark", "type": "dark"}
- ],
- "author": {
- "name": "skymansandy",
- "github": "https://github.com/skymansandy"
- },
- "license": "Apache-2.0",
- "stars": null
-}
-""".trimIndent()
-
-private enum class ThemeOption(val label: String, val colors: JsonCmpColors) {
- Dark("Dark", JsonCmpColors.Dark),
- Light("Light", JsonCmpColors.Light),
- Monokai("Monokai", JsonCmpColors.Monokai),
- Dracula("Dracula", JsonCmpColors.Dracula),
- SolarizedDark("Solarized", JsonCmpColors.SolarizedDark),
-}
-
-@Composable
-fun App() {
-
- MaterialTheme(
- colorScheme = darkColorScheme(),
- ) {
- var searchQuery by remember { mutableStateOf("") }
- var selectedTheme by remember { mutableStateOf(ThemeOption.Dark) }
- val state = rememberJsonEditorState(
- initialJson = sampleJson,
- isEditing = true,
- )
-
- Column(
- modifier = Modifier
- .fillMaxSize()
- .background(selectedTheme.colors.background),
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp, vertical = 4.dp),
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- ) {
- ThemeOption.entries.forEach { theme ->
- FilterChip(
- selected = selectedTheme == theme,
- onClick = { selectedTheme = theme },
- label = { Text(theme.label) },
- )
- }
- }
-
- OutlinedTextField(
- value = searchQuery,
- onValueChange = { searchQuery = it },
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp, vertical = 4.dp),
- label = { Text("Search") },
- singleLine = true,
- )
-
- JsonCMP(
- modifier = Modifier
- .fillMaxSize()
- .weight(1f),
- state = state,
- searchQuery = searchQuery,
- colors = selectedTheme.colors,
- )
- }
- }
-}
diff --git a/composeApp/src/commonMain/kotlin/dev/skymansandy/jsoncmpsample/data/SampleData.kt b/composeApp/src/commonMain/kotlin/dev/skymansandy/jsoncmpsample/data/SampleData.kt
new file mode 100644
index 0000000..8f8dede
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/dev/skymansandy/jsoncmpsample/data/SampleData.kt
@@ -0,0 +1,30 @@
+package dev.skymansandy.jsoncmpsample.data
+
+val sampleJson = """
+{
+ "name": "JsonCMP",
+ "version": "1.0.0",
+ "description": "Kotlin Multiplatform Compose JSON viewer and editor",
+ "platforms": ["Android", "iOS", "JVM Desktop"],
+ "features": {
+ "viewer": true,
+ "editor": true,
+ "search": true,
+ "folding": true,
+ "sorting": true
+ },
+ "themes": [
+ {"name": "Dark", "type": "dark"},
+ {"name": "Light", "type": "light"},
+ {"name": "Monokai", "type": "dark"},
+ {"name": "Dracula", "type": "dark"},
+ {"name": "Solarized Dark", "type": "dark"}
+ ],
+ "author": {
+ "name": "skymansandy",
+ "github": "https://github.com/skymansandy"
+ },
+ "license": "Apache-2.0",
+ "stars": null
+}
+""".trimIndent()
diff --git a/composeApp/src/commonMain/kotlin/dev/skymansandy/jsoncmpsample/model/ThemeOption.kt b/composeApp/src/commonMain/kotlin/dev/skymansandy/jsoncmpsample/model/ThemeOption.kt
new file mode 100644
index 0000000..8be8d75
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/dev/skymansandy/jsoncmpsample/model/ThemeOption.kt
@@ -0,0 +1,11 @@
+package dev.skymansandy.jsoncmpsample.model
+
+import dev.skymansandy.jsoncmp.helper.constants.colors.JsonCmpColors
+
+enum class ThemeOption(val label: String, val colors: JsonCmpColors) {
+ Dark("Dark", JsonCmpColors.Dark),
+ Light("Light", JsonCmpColors.Light),
+ Monokai("Monokai", JsonCmpColors.Monokai),
+ Dracula("Dracula", JsonCmpColors.Dracula),
+ SolarizedDark("Solarized", JsonCmpColors.SolarizedDark),
+}
diff --git a/composeApp/src/commonMain/kotlin/dev/skymansandy/jsoncmpsample/ui/App.kt b/composeApp/src/commonMain/kotlin/dev/skymansandy/jsoncmpsample/ui/App.kt
new file mode 100644
index 0000000..ad3a677
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/dev/skymansandy/jsoncmpsample/ui/App.kt
@@ -0,0 +1,116 @@
+package dev.skymansandy.jsoncmpsample.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MaterialTheme.typography
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.darkColorScheme
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import dev.skymansandy.jsoncmp.JsonCMP
+import dev.skymansandy.jsoncmp.config.rememberJsonEditorState
+import dev.skymansandy.jsoncmpsample.data.sampleJson
+import dev.skymansandy.jsoncmpsample.model.ThemeOption
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun App() {
+ MaterialTheme(
+ colorScheme = darkColorScheme(),
+ ) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("JsonCMP Sample") },
+ )
+ },
+ ) {
+ var searchQuery by remember { mutableStateOf("") }
+ var selectedTheme by remember { mutableStateOf(ThemeOption.Dark) }
+ val state = rememberJsonEditorState(
+ initialJson = sampleJson,
+ isEditing = true,
+ )
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(selectedTheme.colors.background)
+ .padding(it),
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .horizontalScroll(rememberScrollState())
+ .padding(horizontal = 8.dp, vertical = 4.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ ThemeOption.entries.forEach { theme ->
+ FilterChip(
+ selected = selectedTheme == theme,
+ onClick = { selectedTheme = theme },
+ label = { Text(theme.label) },
+ )
+ }
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ OutlinedTextField(
+ value = searchQuery,
+ onValueChange = { searchQuery = it },
+ modifier = Modifier.weight(1f),
+ label = { Text("Search") },
+ singleLine = true,
+ )
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Switch(
+ checked = state.isEditing,
+ onCheckedChange = { state.isEditing = it },
+ )
+ Text(
+ text = if (state.isEditing) "Edit" else "View",
+ style = typography.labelSmall,
+ )
+ }
+ }
+
+ JsonCMP(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ state = state,
+ searchQuery = searchQuery,
+ colors = selectedTheme.colors,
+ )
+ }
+ }
+ }
+}
diff --git a/composeApp/src/iosMain/kotlin/dev/skymansandy/jsoncmpsample/MainViewController.kt b/composeApp/src/iosMain/kotlin/dev/skymansandy/jsoncmpsample/MainViewController.kt
index 3a89fa4..8387820 100644
--- a/composeApp/src/iosMain/kotlin/dev/skymansandy/jsoncmpsample/MainViewController.kt
+++ b/composeApp/src/iosMain/kotlin/dev/skymansandy/jsoncmpsample/MainViewController.kt
@@ -1,6 +1,7 @@
package dev.skymansandy.jsoncmpsample
import androidx.compose.ui.window.ComposeUIViewController
+import dev.skymansandy.jsoncmpsample.ui.App
@Suppress("FunctionNaming")
fun MainViewController() = ComposeUIViewController { App() }
diff --git a/composeApp/src/jvmMain/kotlin/dev/skymansandy/jsoncmpsample/Main.kt b/composeApp/src/jvmMain/kotlin/dev/skymansandy/jsoncmpsample/Main.kt
index 1adcf11..896da8a 100644
--- a/composeApp/src/jvmMain/kotlin/dev/skymansandy/jsoncmpsample/Main.kt
+++ b/composeApp/src/jvmMain/kotlin/dev/skymansandy/jsoncmpsample/Main.kt
@@ -2,6 +2,7 @@ package dev.skymansandy.jsoncmpsample
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
+import dev.skymansandy.jsoncmpsample.ui.App
fun main() {
application {
diff --git a/gradle.properties b/gradle.properties
index f94d15c..d6c7110 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -17,4 +17,4 @@ android.r8.strictFullModeForKeepRules=false
#Publishing
jsoncmp.group=dev.skymansandy
-jsoncmp.version=1.0.0-alpha1
+jsoncmp.version=1.0.0-alpha2
diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig
new file mode 100644
index 0000000..f71ba00
--- /dev/null
+++ b/iosApp/Configuration/Config.xcconfig
@@ -0,0 +1,5 @@
+TEAM_ID=
+PRODUCT_NAME=JsonCMP Sample
+PRODUCT_BUNDLE_IDENTIFIER=dev.skymansandy.jsoncmp.JsonCMPSample$(TEAM_ID)
+CURRENT_PROJECT_VERSION=1
+MARKETING_VERSION=1.0
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..4f19daa
--- /dev/null
+++ b/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -0,0 +1,372 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 058557BB273AAA2400C3D2FC /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA2400C3D2FC /* iOSApp.swift */; };
+ 058557BD273AAA2400C3D2FC /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 058557BC273AAA2400C3D2FC /* ContentView.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 058557BA273AAA2400C3D2FC /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
+ 058557BC273AAA2400C3D2FC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ 7555FF7B242A565900829871 /* JsonCMP Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "JsonCMP Sample.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ B92378962B6B1156000C7307 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 058557D7273AAEEB004C7B11 /* Configuration */ = {
+ isa = PBXGroup;
+ children = (
+ AB3632DC29227652001CCB65 /* Config.xcconfig */,
+ );
+ path = Configuration;
+ sourceTree = "";
+ };
+ 7555FF72242A565900829871 = {
+ isa = PBXGroup;
+ children = (
+ 7555FF7D242A565900829871 /* iosApp */,
+ 058557D7273AAEEB004C7B11 /* Configuration */,
+ 7555FF7C242A565900829871 /* Products */,
+ );
+ sourceTree = "";
+ };
+ 7555FF7C242A565900829871 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 7555FF7B242A565900829871 /* JsonCMP Sample.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 7555FF7D242A565900829871 /* iosApp */ = {
+ isa = PBXGroup;
+ children = (
+ 058557BA273AAA2400C3D2FC /* iOSApp.swift */,
+ 058557BC273AAA2400C3D2FC /* ContentView.swift */,
+ 7555FF8C242A565B00829871 /* Info.plist */,
+ );
+ path = iosApp;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 7555FF7A242A565900829871 /* iosApp */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
+ buildPhases = (
+ F36B0B8F2B60B08F0080A992 /* Compile Kotlin Framework */,
+ 7555FF77242A565900829871 /* Sources */,
+ B92378962B6B1156000C7307 /* Frameworks */,
+ 7555FF79242A565900829871 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = iosApp;
+ productName = iosApp;
+ productReference = 7555FF7B242A565900829871 /* JsonCMP Sample.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 7555FF73242A565900829871 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 1540;
+ LastUpgradeCheck = 1540;
+ ORGANIZATIONNAME = "";
+ };
+ buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 7555FF72242A565900829871;
+ productRefGroup = 7555FF7C242A565900829871 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 7555FF7A242A565900829871 /* iosApp */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 7555FF79242A565900829871 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ F36B0B8F2B60B08F0080A992 /* Compile Kotlin Framework */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ name = "Compile Kotlin Framework";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "cd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 7555FF77242A565900829871 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 058557BB273AAA2400C3D2FC /* iOSApp.swift in Sources */,
+ 058557BD273AAA2400C3D2FC /* ContentView.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 7555FFA3242A565B00829871 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 7555FFA4242A565B00829871 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 7555FFA6242A565B00829871 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_TEAM = "${TEAM_ID}";
+ ENABLE_PREVIEWS = YES;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ );
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = iosApp/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = "${PRODUCT_NAME}";
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-lsqlite3",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}";
+ PRODUCT_NAME = "${PRODUCT_NAME}";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 7555FFA7242A565B00829871 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_TEAM = "${TEAM_ID}";
+ ENABLE_PREVIEWS = YES;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ );
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = iosApp/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = "${PRODUCT_NAME}";
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-lsqlite3",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}";
+ PRODUCT_NAME = "${PRODUCT_NAME}";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 7555FFA3242A565B00829871 /* Debug */,
+ 7555FFA4242A565B00829871 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 7555FFA6242A565B00829871 /* Debug */,
+ 7555FFA7242A565B00829871 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 7555FF73242A565900829871 /* Project object */;
+}
diff --git a/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/gosandy.xcuserdatad/xcschemes/xcschememanagement.plist b/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/gosandy.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..ee3458d
--- /dev/null
+++ b/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/gosandy.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/iosApp/iosApp.xcodeproj/xcuserdata/gosandy.xcuserdatad/xcschemes/iosApp.xcscheme b/iosApp/iosApp.xcodeproj/xcuserdata/gosandy.xcuserdatad/xcschemes/iosApp.xcscheme
new file mode 100644
index 0000000..94263e4
--- /dev/null
+++ b/iosApp/iosApp.xcodeproj/xcuserdata/gosandy.xcuserdatad/xcschemes/iosApp.xcscheme
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/iosApp/iosApp.xcodeproj/xcuserdata/gosandy.xcuserdatad/xcschemes/xcschememanagement.plist b/iosApp/iosApp.xcodeproj/xcuserdata/gosandy.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..fa59f97
--- /dev/null
+++ b/iosApp/iosApp.xcodeproj/xcuserdata/gosandy.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ SchemeUserState
+
+ iosApp.xcscheme
+
+ orderHint
+ 0
+
+
+
+
diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift
new file mode 100644
index 0000000..264c98d
--- /dev/null
+++ b/iosApp/iosApp/ContentView.swift
@@ -0,0 +1,18 @@
+import UIKit
+import SwiftUI
+import JsonCMPSample
+
+struct ComposeView: UIViewControllerRepresentable {
+ func makeUIViewController(context: Context) -> UIViewController {
+ MainViewControllerKt.MainViewController()
+ }
+
+ func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
+}
+
+struct ContentView: View {
+ var body: some View {
+ ComposeView()
+ .ignoresSafeArea()
+ }
+}
diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist
new file mode 100644
index 0000000..ba6b757
--- /dev/null
+++ b/iosApp/iosApp/Info.plist
@@ -0,0 +1,13 @@
+
+
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+
+
diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift
new file mode 100644
index 0000000..927e0b9
--- /dev/null
+++ b/iosApp/iosApp/iOSApp.swift
@@ -0,0 +1,10 @@
+import SwiftUI
+
+@main
+struct iOSApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}
diff --git a/json-cmp/src/commonMain/kotlin/dev/skymansandy/jsoncmp/component/editor/CodeEditor.kt b/json-cmp/src/commonMain/kotlin/dev/skymansandy/jsoncmp/component/editor/CodeEditor.kt
index 3d04685..41f6c2f 100644
--- a/json-cmp/src/commonMain/kotlin/dev/skymansandy/jsoncmp/component/editor/CodeEditor.kt
+++ b/json-cmp/src/commonMain/kotlin/dev/skymansandy/jsoncmp/component/editor/CodeEditor.kt
@@ -2,16 +2,14 @@ package dev.skymansandy.jsoncmp.component.editor
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -23,7 +21,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.layout.Layout
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -47,33 +47,90 @@ internal fun CodeEditor(
}
val horizontalScrollState = rememberScrollState()
+ val verticalScrollState = rememberScrollState()
val lineCount = remember(textFieldValue.text) { textFieldValue.text.count { it == '\n' } + 1 }
val numDigits = remember(lineCount) { lineCount.toString().length }
+ var textLayoutResult by remember { mutableStateOf(null) }
+
+ val highlighted: AnnotatedString = remember(textFieldValue.text, searchQuery, colors) {
+ highlightJson(
+ text = textFieldValue.text,
+ searchQuery = searchQuery,
+ colors = colors,
+ )
+ }
Row(
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 200.dp)
- .height(IntrinsicSize.Min)
+ .verticalScroll(verticalScrollState)
.background(colors.background),
) {
- // Line number gutter
+ // Line number gutter — positioned using actual text layout line offsets
val borderColor = colors.gutterBorder
- Column(
+ LineNumberGutter(
+ lineCount = lineCount,
+ numDigits = numDigits,
+ textLayoutResult = textLayoutResult,
+ colors = colors,
+ borderColor = borderColor,
+ )
+
+ BasicTextField(
+ value = textFieldValue.copy(annotatedString = highlighted),
+ onValueChange = { newValue ->
+ textFieldValue = newValue
+ lastSyncedRaw = newValue.text
+ state.updateRawJson(newValue.text)
+ },
+ onTextLayout = { textLayoutResult = it },
+ textStyle = monoStyle,
+ cursorBrush = SolidColor(colors.key),
+ modifier = Modifier
+ .weight(1f)
+ .horizontalScroll(horizontalScrollState)
+ .padding(start = 8.dp, end = 16.dp),
+ )
+ }
+}
+
+@Composable
+private fun LineNumberGutter(
+ lineCount: Int,
+ numDigits: Int,
+ textLayoutResult: TextLayoutResult?,
+ colors: JsonCmpColors,
+ borderColor: androidx.compose.ui.graphics.Color,
+) {
+ if (textLayoutResult == null || textLayoutResult.lineCount < lineCount) {
+ // Fallback: render without alignment until layout is available
+ Box(
modifier = Modifier
- .fillMaxHeight()
.background(colors.gutterBackground)
.drawBehind {
val x = size.width
- drawLine(
- borderColor,
- Offset(x, 0f),
- Offset(x, size.height),
- strokeWidth = 1.dp.toPx(),
- )
+ drawLine(borderColor, Offset(x, 0f), Offset(x, size.height), strokeWidth = 1.dp.toPx())
}
.padding(start = 12.dp, end = 8.dp),
) {
+ androidx.compose.foundation.layout.Column {
+ for (i in 1..lineCount) {
+ Text(
+ text = i.toString().padStart(numDigits),
+ style = monoStyle,
+ color = colors.lineNumber,
+ softWrap = false,
+ )
+ }
+ }
+ }
+ return
+ }
+
+ // Use Layout to position each line number at the exact Y offset from the text layout
+ Layout(
+ content = {
for (i in 1..lineCount) {
Text(
text = i.toString().padStart(numDigits),
@@ -82,31 +139,30 @@ internal fun CodeEditor(
softWrap = false,
)
}
+ },
+ modifier = Modifier
+ .background(colors.gutterBackground)
+ .drawBehind {
+ val x = size.width
+ drawLine(borderColor, Offset(x, 0f), Offset(x, size.height), strokeWidth = 1.dp.toPx())
+ }
+ .padding(start = 12.dp, end = 8.dp),
+ ) { measurables, constraints ->
+ val textLayout = textLayoutResult
+ val placeables = measurables.map { it.measure(constraints.copy(minWidth = 0, minHeight = 0)) }
+ val width = placeables.maxOfOrNull { it.width } ?: 0
+ val height = if (textLayout != null && textLayout.lineCount > 0) {
+ textLayout.getLineBottom(textLayout.lineCount - 1).toInt()
+ } else {
+ placeables.sumOf { it.height }
}
- // Text editor with syntax highlighting
- val highlighted: AnnotatedString = remember(textFieldValue.text, searchQuery, colors) {
- highlightJson(
- text = textFieldValue.text,
- searchQuery = searchQuery,
- colors = colors,
- )
+ layout(width, height) {
+ placeables.forEachIndexed { index, placeable ->
+ val y = textLayout?.getLineTop(index)?.toInt() ?: (index * (placeables.firstOrNull()?.height ?: 0))
+ placeable.placeRelative(0, y)
+ }
}
-
- BasicTextField(
- value = textFieldValue.copy(annotatedString = highlighted),
- onValueChange = { newValue ->
- textFieldValue = newValue
- lastSyncedRaw = newValue.text
- state.updateRawJson(newValue.text)
- },
- textStyle = monoStyle,
- cursorBrush = SolidColor(colors.key),
- modifier = Modifier
- .weight(1f)
- .horizontalScroll(horizontalScrollState)
- .padding(start = 8.dp, end = 16.dp),
- )
}
}
diff --git a/json-cmp/src/commonMain/kotlin/dev/skymansandy/jsoncmp/component/editor/EditorToolbar.kt b/json-cmp/src/commonMain/kotlin/dev/skymansandy/jsoncmp/component/editor/EditorToolbar.kt
index e39edd7..c4706f0 100644
--- a/json-cmp/src/commonMain/kotlin/dev/skymansandy/jsoncmp/component/editor/EditorToolbar.kt
+++ b/json-cmp/src/commonMain/kotlin/dev/skymansandy/jsoncmp/component/editor/EditorToolbar.kt
@@ -1,6 +1,7 @@
package dev.skymansandy.jsoncmp.component.editor
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -18,8 +19,12 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
+import androidx.compose.material3.TooltipBox
+import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -50,33 +55,47 @@ internal fun EditorToolbar(
.background(colors.gutterBackground)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.End,
) {
// Format: Beautify / Compact toggle
- IconButton(
- onClick = { state.format(compact = !state.isCompact) },
- modifier = Modifier.size(36.dp),
+ val formatLabel = if (state.isCompact) "Beautify" else "Compact"
+ TooltipBox(
+ positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
+ tooltip = { PlainTooltip { Text(formatLabel) } },
+ state = rememberTooltipState(),
) {
- Icon(
- imageVector = if (state.isCompact) Icons.AutoMirrored.Filled.FormatIndentIncrease else Icons.Default.FormatAlignJustify,
- contentDescription = if (state.isCompact) "Beautify" else "Compact",
- tint = colors.key,
- modifier = Modifier.size(18.dp),
- )
+ IconButton(
+ onClick = { state.format(compact = !state.isCompact) },
+ modifier = Modifier.size(36.dp),
+ ) {
+ Icon(
+ imageVector = if (state.isCompact) Icons.AutoMirrored.Filled.FormatIndentIncrease else Icons.Default.FormatAlignJustify,
+ contentDescription = formatLabel,
+ tint = colors.key,
+ modifier = Modifier.size(18.dp),
+ )
+ }
}
ToolbarDivider(colors)
// Sort
- IconButton(
- onClick = { showSortSheet = true },
- modifier = Modifier.size(36.dp),
+ TooltipBox(
+ positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
+ tooltip = { PlainTooltip { Text("Sort keys") } },
+ state = rememberTooltipState(),
) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.Sort,
- contentDescription = "Sort keys",
- tint = colors.key,
- modifier = Modifier.size(18.dp),
- )
+ IconButton(
+ onClick = { showSortSheet = true },
+ modifier = Modifier.size(36.dp),
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.Sort,
+ contentDescription = "Sort keys",
+ tint = colors.key,
+ modifier = Modifier.size(18.dp),
+ )
+ }
}
}
diff --git a/json-cmp/src/commonMain/kotlin/dev/skymansandy/jsoncmp/component/viewer/JsonViewer.kt b/json-cmp/src/commonMain/kotlin/dev/skymansandy/jsoncmp/component/viewer/JsonViewer.kt
index a12af57..15d8b94 100644
--- a/json-cmp/src/commonMain/kotlin/dev/skymansandy/jsoncmp/component/viewer/JsonViewer.kt
+++ b/json-cmp/src/commonMain/kotlin/dev/skymansandy/jsoncmp/component/viewer/JsonViewer.kt
@@ -3,8 +3,10 @@ package dev.skymansandy.jsoncmp.component.viewer
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@@ -52,7 +54,9 @@ internal fun JsonViewer(
modifier = Modifier.fillMaxWidth(),
) {
Column(
- modifier = Modifier.fillMaxWidth(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState()),
) {
for (line in visibleLines) {
val isFolded = line.foldId != null && foldState[line.foldId] == true
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 55b48cc..6a5d0d8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -33,3 +33,4 @@ plugins {
include(":json-cmp")
include(":composeApp")
+include(":androidApp")