diff --git a/README.md b/README.md index d7a0b42..44b3ceb 100644 --- a/README.md +++ b/README.md @@ -1 +1,14 @@ -# TextExplorer \ No newline at end of file +# TextExplorer +This is a tool designed for the exploration and comparison of variants of textual data. + +## Usage + +TODO + +## Development + +TODO + +## Support + +If you have any problems or questions about this project, please get in touch. You can also [open an issue](https://github.com/Paulanerus/TextExplorer/issues) on GitHub. \ No newline at end of file diff --git a/api/src/main/kotlin/dev/paulee/api/data/IDataService.kt b/api/src/main/kotlin/dev/paulee/api/data/IDataService.kt index c9f2272..dafceb6 100644 --- a/api/src/main/kotlin/dev/paulee/api/data/IDataService.kt +++ b/api/src/main/kotlin/dev/paulee/api/data/IDataService.kt @@ -13,6 +13,8 @@ interface IDataService : Closeable { fun getSelectedPool(): String + fun hasSelectedPool(): Boolean + fun getAvailablePools(): Set fun getPage(query: String, pageCount: Int): Pair>, Map>>> diff --git a/core/src/main/kotlin/dev/paulee/core/data/DataServiceImpl.kt b/core/src/main/kotlin/dev/paulee/core/data/DataServiceImpl.kt index 4773ef8..eb4b22e 100644 --- a/core/src/main/kotlin/dev/paulee/core/data/DataServiceImpl.kt +++ b/core/src/main/kotlin/dev/paulee/core/data/DataServiceImpl.kt @@ -260,6 +260,8 @@ class DataServiceImpl(private val storageProvider: IStorageProvider) : IDataServ override fun getSelectedPool(): String = "${this.currentPool}.${this.currentField}" + override fun hasSelectedPool(): Boolean = this.currentPool != null && this.currentField != null + override fun getAvailablePools(): Set = dataPools.filter { it.value.fields.any { it.value } } .flatMap { entry -> entry.value.fields.filter { it.value }.map { "${entry.key}.${it.key.substringBefore(".")}" } diff --git a/core/src/main/kotlin/dev/paulee/core/plugin/PluginServiceImpl.kt b/core/src/main/kotlin/dev/paulee/core/plugin/PluginServiceImpl.kt index ad15a26..d0163a0 100644 --- a/core/src/main/kotlin/dev/paulee/core/plugin/PluginServiceImpl.kt +++ b/core/src/main/kotlin/dev/paulee/core/plugin/PluginServiceImpl.kt @@ -14,6 +14,7 @@ import kotlin.io.path.isDirectory import kotlin.io.path.walk import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.functions +import kotlin.reflect.full.primaryConstructor class PluginServiceImpl : IPluginService { @@ -86,9 +87,20 @@ class PluginServiceImpl : IPluginService { val func = taggable::class.functions.find { it.name == "tag" } ?: return null - return func.findAnnotation() + val annotation = func.findAnnotation() ?: return null + + val fields = getAllFields(plugin) + + if (fields.isEmpty()) return annotation + + if (annotation.fields.none { it in fields }) return null + + return annotation } + private fun getAllFields(plugin: IPlugin): Set = this.getDataInfo(plugin)?.sources.orEmpty() + .flatMap { it.primaryConstructor?.parameters.orEmpty().mapNotNull { it.name } }.toSet() + private fun getPluginEntryPoint(path: Path): String? = JarFile(path.toFile()).use { return it.manifest.mainAttributes.getValue("Main-Class") } diff --git a/gradle.properties b/gradle.properties index a308d36..81ca371 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ kotlin.version=2.1.0 compose.version=1.7.1 lucene.version=10.0.0 -api.version=0.32.1 -core.version=0.30.1 -ui.version=0.21.0 -app.version=1.6.0 +api.version=0.32.2 +core.version=0.30.3 +ui.version=0.21.7 +app.version=1.7.0 diff --git a/ui/src/main/kotlin/dev/paulee/ui/Config.kt b/ui/src/main/kotlin/dev/paulee/ui/Config.kt index 6e34c4f..c9e9e20 100644 --- a/ui/src/main/kotlin/dev/paulee/ui/Config.kt +++ b/ui/src/main/kotlin/dev/paulee/ui/Config.kt @@ -13,28 +13,35 @@ object Config { var noWidthRestriction = false + var selectedPool = "" + private var configFile = "config" private var configPath = Path(configFile) + private val hiddenColumns = mutableMapOf>() + fun save() { this.configPath.bufferedWriter().use { writer -> - - this::class.memberProperties - .filter { it.visibility == KVisibility.PUBLIC && it is KMutableProperty<*> } + this::class.memberProperties.filter { it.visibility == KVisibility.PUBLIC && it is KMutableProperty<*> } .forEach { val value = it.getter.call(this) writer.write("${it.name} = $value\n") writer.newLine() } + + this.hiddenColumns.forEach { + writer.write("${it.key} = ${it.value}\n") + writer.newLine() + } } } fun load(path: Path) { this.configPath = path.resolve(configFile) - if(this.configPath.notExists()) return + if (this.configPath.notExists()) return this.configPath.bufferedReader().useLines { lines -> lines.filter { it.contains("=") }.forEach { @@ -60,9 +67,21 @@ object Config { ) }.onFailure { e -> println("Failed to set value for $field (${e.message}).") } } else { - //TODO + value.takeIf { it.startsWith("[") && it.endsWith("]") } + ?.trim('[', ']') + ?.replace(" ", "") + ?.split(",") + ?.mapNotNull { it.toIntOrNull() } + ?.toSet() + ?.let { this.hiddenColumns[field] = it } } } } } + + fun setHidden(id: String, ids: Set) { + this.hiddenColumns[id] = ids + } + + fun getHidden(id: String): Set = this.hiddenColumns[id] ?: emptySet() } \ No newline at end of file diff --git a/ui/src/main/kotlin/dev/paulee/ui/TextExplorerUI.kt b/ui/src/main/kotlin/dev/paulee/ui/TextExplorerUI.kt index 0c62f04..fb27cd0 100644 --- a/ui/src/main/kotlin/dev/paulee/ui/TextExplorerUI.kt +++ b/ui/src/main/kotlin/dev/paulee/ui/TextExplorerUI.kt @@ -32,7 +32,7 @@ import kotlin.io.path.* class TextExplorerUI( private val pluginService: IPluginService, private val dataService: IDataService, - private val diffService: DiffService + private val diffService: DiffService, ) { private val appDir = Path(System.getProperty("user.home")).resolve(".textexplorer") @@ -62,6 +62,8 @@ class TextExplorerUI( val size = this.dataService.loadDataPools(dataDir, this.pluginService.getAllDataInfos()) println("Loaded $size data pools") + + if (Config.selectedPool.isNotEmpty()) this.dataService.selectDataPool(Config.selectedPool) } @Composable @@ -109,6 +111,7 @@ class TextExplorerUI( DropdownMenuItem( onClick = { dataService.selectDataPool(item.first) + Config.selectedPool = item.first poolSelected = !poolSelected if (selectedText != item.second) { @@ -128,7 +131,7 @@ class TextExplorerUI( DropDownMenu( modifier = Modifier.align(Alignment.TopEnd), - items = listOf("Load Plugin", "Width Limit"), + items = listOf("Load Plugin", "Width Limit", "Plugin Info"), clicked = { when (it) { "Load Plugin" -> isOpened = true @@ -136,6 +139,10 @@ class TextExplorerUI( Config.noWidthRestriction = !Config.noWidthRestriction widthLimitWrapper = !widthLimitWrapper } + + "Plugin Info" -> { + println("Show plugin info") + } } }) @@ -207,7 +214,7 @@ class TextExplorerUI( showTable = true }, modifier = Modifier.height(70.dp).padding(horizontal = 10.dp), - enabled = text.isNotEmpty() && text.isNotBlank() + enabled = text.isNotBlank() && dataService.hasSelectedPool() ) { Icon(Icons.Default.Search, contentDescription = "Search") } @@ -230,6 +237,7 @@ class TextExplorerUI( TableView( modifier = Modifier.weight(1f), + dataService.getSelectedPool(), indexStrings = indexStrings, columns = header, data = data, @@ -315,6 +323,8 @@ class TextExplorerUI( } private fun loadPlugin(path: Path): Boolean { + val parentPath = path.parent + val pluginPath = pluginsDir.resolve(path.name) if (pluginPath.exists()) return true @@ -325,13 +335,22 @@ class TextExplorerUI( if (plugin == null) return false - this.pluginService.getDataInfo(plugin)?.let { - if (it.sources.isEmpty()) return@let + this.pluginService.getDataInfo(plugin)?.let { dataInfo -> + if (dataInfo.sources.isEmpty()) return@let + + this.pluginService.getDataSources(dataInfo.name).forEach { + val name = it.let { if (it.endsWith(".csv")) it else "$it.csv" } + + val dataSourcePath = parentPath.resolve(name) + + if (dataSourcePath.exists()) dataSourcePath.copyTo(this.dataDir.resolve(name), true) + else println("No source file for '$it' in plugin dir.") + } val poolsEmpty = this.dataService.getAvailablePools().isEmpty() - if (this.dataService.createDataPool(it, dataDir)) { - println("Created data pool for ${it.name}") + if (this.dataService.createDataPool(dataInfo, dataDir)) { + println("Created data pool for ${dataInfo.name}") this.dataService.getAvailablePools().firstOrNull()?.let { if (!poolsEmpty) return@let @@ -340,7 +359,7 @@ class TextExplorerUI( this.poolSelected = !this.poolSelected } - } else println("Failed to create data pool for ${it.name}") + } else println("Failed to create data pool for ${dataInfo.name}") } return true } diff --git a/ui/src/main/kotlin/dev/paulee/ui/components/DiffViewer.kt b/ui/src/main/kotlin/dev/paulee/ui/components/DiffViewer.kt index ca3a14b..9356a31 100644 --- a/ui/src/main/kotlin/dev/paulee/ui/components/DiffViewer.kt +++ b/ui/src/main/kotlin/dev/paulee/ui/components/DiffViewer.kt @@ -1,6 +1,6 @@ package dev.paulee.ui.components -import androidx.compose.foundation.* +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* @@ -25,18 +25,105 @@ import dev.paulee.api.plugin.Taggable import dev.paulee.ui.HeatmapText import dev.paulee.ui.MarkedText -@OptIn(ExperimentalFoundationApi::class) @Composable -fun DiffView( +fun DiffViewerWindow( diffService: DiffService, - entries: List>, - modifier: Modifier = Modifier, + pluginService: IPluginService, + selected: String, + selectedRows: List>, + onClose: () -> Unit, ) { - val horizontalScrollState = rememberScrollState() + fun getTagName(taggable: Taggable?): String? { + val plugin = taggable as? IPlugin ?: return null + + val pluginName = pluginService.getPluginMetadata(plugin)?.name ?: return null + + val viewFilterName = pluginService.getViewFilter(plugin)?.name ?: pluginName + + return when { + viewFilterName.isNotEmpty() -> viewFilterName + pluginName.isNotEmpty() -> pluginName + else -> null + } + } + + val pool = selected.substringBefore(".") + + val associatedPlugins = pluginService.getPlugins().filter { pluginService.getDataInfo(it)?.name == pool } + + val tagPlugins = associatedPlugins.mapNotNull { it as? Taggable } + var selectedTaggablePlugin = tagPlugins.firstOrNull() + + val viewFilter = associatedPlugins.mapNotNull { pluginService.getViewFilter(it) }.filter { it.global } + .flatMap { it.fields.filter { it.isNotBlank() }.toList() }.toSet() + + var selectedText by remember { mutableStateOf(getTagName(selectedTaggablePlugin) ?: "") } + var showPopup by remember { mutableStateOf(false) } + var selected by remember { mutableStateOf(false) } + + val entries = remember(selected) { + if (selected) { + val tagFilter = pluginService.getViewFilter(selectedTaggablePlugin as IPlugin)?.fields.orEmpty() + .filter { it.isNotBlank() } + + selectedRows.map { it.filterKeys { key -> tagFilter.isEmpty() || (key in tagFilter) } } + } else selectedRows.map { it.filterKeys { key -> viewFilter.isEmpty() || (key in viewFilter) } } + } + + Window(onCloseRequest = onClose, title = "DiffViewer") { + MaterialTheme { + Box(modifier = Modifier.fillMaxSize()) { + Box(modifier = Modifier.align(Alignment.TopStart).padding(16.dp)) { + + if (selectedTaggablePlugin == null) return@Box + + Text("Tagger:", fontWeight = FontWeight.Bold) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 4.dp, top = 8.dp) + ) { + Text( + selectedText, + fontSize = 14.sp, + modifier = Modifier.then(if (tagPlugins.size > 1) Modifier.clickable { showPopup = true } + else Modifier)) + + Checkbox(checked = selected, onCheckedChange = { selected = it }) + } + + DropdownMenu( + expanded = showPopup, onDismissRequest = { showPopup = false }) { + val menuItems = tagPlugins.mapNotNull { + val name = getTagName(it) ?: return@mapNotNull null + + Pair(name, it) + } + menuItems.forEach { item -> + DropdownMenuItem( + onClick = { + selectedTaggablePlugin = item.second + selectedText = item.first + showPopup = false + }) { + Text(item.first) + } + } + } + } + + Box(modifier = Modifier.fillMaxSize()) { + if (selected) TagView(entries, selectedTaggablePlugin, Modifier.align(Alignment.CenterStart)) + else DiffView(diffService, entries, Modifier.align(Alignment.CenterStart)) + } + } + } + } +} - val grouped = entries.flatMap { it.entries } - .groupBy({ it.key }, { it.value }) - .filterValues { it.isNotEmpty() } +@Composable +private fun TagView(entries: List>, taggable: Taggable?, modifier: Modifier = Modifier) { + val grouped = entries.flatMap { it.entries }.groupBy({ it.key }, { it.value }).filterValues { it.isNotEmpty() } val greatestSize = grouped.values.maxOfOrNull { it.size } ?: 0 @@ -50,8 +137,7 @@ fun DiffView( val headerTextStyle = LocalTextStyle.current.copy(fontWeight = FontWeight.Bold) val maxWidth = columns.maxOf { - val headerWidthPx = - textMeasurer.measure(text = AnnotatedString(it), style = headerTextStyle).size.width + val headerWidthPx = textMeasurer.measure(text = AnnotatedString(it), style = headerTextStyle).size.width with(density) { headerWidthPx.toDp() + 16.dp } } @@ -86,143 +172,111 @@ fun DiffView( } } - Column(modifier = Modifier.horizontalScroll(horizontalScrollState)) { + Column { (0 until greatestSize).forEach { index -> val columnName = columns.getOrNull(currentColumnIndex) ?: return@forEach val values = grouped[columnName] ?: return@forEach - Row(modifier = Modifier.fillMaxWidth()) { - val value = values.getOrNull(index) ?: "" + val value = values.getOrNull(index) ?: "" - val change = diffService.getDiff(values[0], value) + val tags = taggable?.tag(columnName, value).orEmpty() - SelectionContainer { - if (index == 0) { - Text(value) - Spacer(Modifier.height(40.dp)) - } else { - if (value.isEmpty()) Text(value) - else HeatmapText(change, values[0], textAlign = TextAlign.Left) - } - } + SelectionContainer { + MarkedText( + text = value, + highlights = tags, + textAlign = TextAlign.Center, + ) } } } } } - - HorizontalScrollbar( - adapter = rememberScrollbarAdapter(horizontalScrollState), modifier = Modifier.padding(top = 10.dp) - ) } } } @Composable -fun DiffViewerWindow( +private fun DiffView( diffService: DiffService, - pluginService: IPluginService, - selected: String, - selectedRows: List>, - onClose: () -> Unit, + entries: List>, + modifier: Modifier = Modifier, ) { - fun getTagName(taggable: Taggable?): String? { - val plugin = taggable as? IPlugin ?: return null + val grouped = entries.flatMap { it.entries }.groupBy({ it.key }, { it.value }).filterValues { it.isNotEmpty() } - val pluginName = pluginService.getPluginMetadata(plugin)?.name ?: return null - - val viewFilterName = pluginService.getViewFilter(plugin)?.name ?: pluginName - - return when { - viewFilterName.isNotEmpty() -> viewFilterName - pluginName.isNotEmpty() -> pluginName - else -> null - } - } + val greatestSize = grouped.values.maxOfOrNull { it.size } ?: 0 - val (pool, select) = selected.split(".", limit = 2) + val columns = entries.flatMap { it.keys }.distinct() - val associatedPlugins = pluginService.getPlugins().filter { pluginService.getDataInfo(it)?.name == pool } + var currentColumnIndex by remember { mutableStateOf(0) } - val tagPlugins = associatedPlugins.mapNotNull { it as? Taggable } - var selectedTagger = tagPlugins.firstOrNull() + val textMeasurer = rememberTextMeasurer() + val density = LocalDensity.current - val viewFilter = associatedPlugins.mapNotNull { pluginService.getViewFilter(it) }.filter { it.global } - .flatMap { it.fields.toList() }.toSet() + val headerTextStyle = LocalTextStyle.current.copy(fontWeight = FontWeight.Bold) - var selectedText by remember { mutableStateOf(getTagName(selectedTagger) ?: "") } - var showPopup by remember { mutableStateOf(false) } - var selected by remember { mutableStateOf(false) } + val maxWidth = columns.maxOf { + val headerWidthPx = textMeasurer.measure(text = AnnotatedString(it), style = headerTextStyle).size.width - val entries = remember(selected) { - if (selected) selectedRows.map { it.filterKeys { key -> key in viewFilter } } // TODO apply plugin specific view filter. - else selectedRows.map { it.filterKeys { key -> key in viewFilter } } + with(density) { headerWidthPx.toDp() + 16.dp } } - Window(onCloseRequest = onClose, title = "DiffViewer") { - MaterialTheme { - Box(modifier = Modifier.fillMaxSize()) { - Box(modifier = Modifier.align(Alignment.TopStart).padding(16.dp)) { - - if (selectedTagger == null) return@Box + Box(modifier = modifier) { + Column { + Box(modifier = Modifier.padding(horizontal = 32.dp)) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + if (columns.isEmpty()) return@Column - Text("Tagger:", fontWeight = FontWeight.Bold) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + IconButton( + onClick = { if (currentColumnIndex > 0) currentColumnIndex-- }, + enabled = currentColumnIndex > 0 + ) { + Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Previous column") + } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(start = 4.dp, top = 8.dp) - ) { Text( - selectedText, - fontSize = 14.sp, - modifier = Modifier - .then(if (tagPlugins.size > 1) Modifier.clickable { showPopup = true } - else Modifier)) + text = columns[currentColumnIndex], + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 4.dp, top = 12.dp, end = 4.dp).width(maxWidth) + ) - Checkbox(checked = selected, onCheckedChange = { selected = it }) + IconButton( + onClick = { if (currentColumnIndex < columns.size - 1) currentColumnIndex++ }, + enabled = currentColumnIndex < columns.size - 1 + ) { + Icon(Icons.AutoMirrored.Default.ArrowForward, contentDescription = "Next column") + } } - DropdownMenu( - expanded = showPopup, onDismissRequest = { showPopup = false }) { - val menuItems = tagPlugins.mapNotNull { - val name = getTagName(it) ?: return@mapNotNull null + Column { + (0 until greatestSize).forEach { index -> + val columnName = columns.getOrNull(currentColumnIndex) ?: return@forEach - Pair(name, it) - } - menuItems.forEach { item -> - DropdownMenuItem( - onClick = { - selectedTagger = item.second - selectedText = item.first - showPopup = false - }) { - Text(item.first) - } - } - } - } + val values = grouped[columnName] ?: return@forEach - Box(modifier = Modifier.fillMaxSize()) { - if (selected) { - Column( - modifier = Modifier.align(Alignment.Center), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - entries.forEach { entry -> + val value = values.getOrNull(index) ?: "" - val tags = selectedTagger?.tag("text", entry.values.first()).orEmpty() + val change = diffService.getDiff(values[0], value) - SelectionContainer { - MarkedText( - text = entry.values.first(), - highlights = tags, - textAlign = TextAlign.Center + SelectionContainer { + if (index == 0) { + Text(value) + Spacer(Modifier.height(40.dp)) + } else { + if (value.isEmpty()) Text(value) + else HeatmapText( + change, + values[0], + textAlign = TextAlign.Left, ) } } } - } else DiffView(diffService, entries, Modifier.align(Alignment.Center)) + } } } } diff --git a/ui/src/main/kotlin/dev/paulee/ui/components/TableView.kt b/ui/src/main/kotlin/dev/paulee/ui/components/TableView.kt index 191f5c9..fd1e424 100644 --- a/ui/src/main/kotlin/dev/paulee/ui/components/TableView.kt +++ b/ui/src/main/kotlin/dev/paulee/ui/components/TableView.kt @@ -31,6 +31,7 @@ var widthLimitWrapper by mutableStateOf(Config.noWidthRestriction) @Composable fun TableView( modifier: Modifier = Modifier, + pool: String, indexStrings: Set = emptySet(), columns: List, data: List>, @@ -38,11 +39,12 @@ fun TableView( onRowSelect: (List>) -> Unit, clicked: () -> Unit = {}, ) { - val scrollState = rememberScrollState() + val hiddenColumnsScrollState = rememberScrollState() + val horizontalScrollState = rememberScrollState() val verticalScrollState = rememberLazyListState() var selectedRows by remember { mutableStateOf(setOf()) } - var hiddenColumns by remember { mutableStateOf(setOf()) } + var hiddenColumns by remember { mutableStateOf(Config.getHidden(pool)) } val textMeasurer = rememberTextMeasurer() val density = LocalDensity.current @@ -75,34 +77,53 @@ fun TableView( Box(modifier = modifier) { Column(modifier = Modifier.fillMaxSize()) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { IconButton( onClick = { selectedRows = emptySet() onRowSelect(emptyList()) - }, modifier = Modifier.align(Alignment.CenterStart), enabled = selectedRows.isNotEmpty() + }, enabled = selectedRows.isNotEmpty(), + modifier = Modifier.width(48.dp).padding(bottom = 12.dp) ) { Icon(Icons.Default.Delete, contentDescription = "Delete") } - Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.align(Alignment.Center)) { - columns.forEachIndexed { index, column -> - Button(onClick = { - hiddenColumns = if (hiddenColumns.contains(index)) - hiddenColumns - index - else - hiddenColumns + index - - }, colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray)) { - Text(column) + Column( + modifier = Modifier.weight(1f).padding(horizontal = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.horizontalScroll(hiddenColumnsScrollState) + ) { + columns.forEachIndexed { index, column -> + Button(onClick = { + hiddenColumns = if (hiddenColumns.contains(index)) + hiddenColumns - index + else + hiddenColumns + index + + Config.setHidden(pool, hiddenColumns) + }, colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray)) { + Text(column) + } } } + + HorizontalScrollbar( + adapter = rememberScrollbarAdapter(hiddenColumnsScrollState), + modifier = Modifier.padding(top = 4.dp) + ) } Button( onClick = clicked, enabled = selectedRows.isNotEmpty(), - modifier = Modifier.width(120.dp).align(Alignment.CenterEnd) + modifier = Modifier.width(120.dp).padding(bottom = 12.dp) ) { if (selectedRows.size <= 1) Text("View") else Text("View Diff") @@ -112,7 +133,7 @@ fun TableView( Spacer(modifier = Modifier.height(4.dp)) Row( - modifier = Modifier.fillMaxWidth().horizontalScroll(scrollState).background(Color.Gray) + modifier = Modifier.fillMaxWidth().horizontalScroll(horizontalScrollState).background(Color.Gray) ) { columns.forEachIndexed { index, columnName -> if (hiddenColumns.contains(index)) return@forEachIndexed @@ -144,7 +165,7 @@ fun TableView( modifier = Modifier.fillMaxSize() ) { Box( - modifier = Modifier.horizontalScroll(scrollState) + modifier = Modifier.horizontalScroll(horizontalScrollState) ) { LazyColumn(state = verticalScrollState) { items(data.size) { rowIndex -> @@ -211,7 +232,8 @@ fun TableView( ) HorizontalScrollbar( - adapter = rememberScrollbarAdapter(scrollState), modifier = Modifier.align(Alignment.BottomCenter) + adapter = rememberScrollbarAdapter(horizontalScrollState), + modifier = Modifier.align(Alignment.BottomCenter) ) } }