Add app-native interactive Git dialogs

- Open an interactive staging dialog for `git add -i`, `git stage -i`, and
  `git add --interactive`, allowing paths to be selected before staging.
- Open an interactive rebase dialog for `git rebase -i` / `--interactive`, with
  pick, squash, drop, commit-message editing, and reorder controls.
- Keep the existing synthetic runtime behavior available for direct engine/test calls,
  while making app-entered interactive commands show real UI instead of auto-staging.
- Center manpage search navigation using actual text-layout highlight bounds instead
  of approximate line and column math.
- Make editor dialogs use a full-screen IME-padded outer scroll container so controls
  remain reachable when the keyboard pans or resizes the dialog.
This commit is contained in:
Joe Tretter
2026-05-09 15:41:55 -05:00
parent 0acf8c9ab0
commit f49e8a96b1
9 changed files with 708 additions and 120 deletions

View File

@@ -19,8 +19,8 @@ android {
applicationId = "solutions.tretter.githugandroid"
minSdk = 26
targetSdk = 35
versionCode = 131
versionName = "0.1.130"
versionCode = 132
versionName = "0.1.131"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true

View File

@@ -70,6 +70,8 @@ fun GitHugApp() {
var hasRestoredProgress by remember { mutableStateOf(false) }
var editorState by remember { mutableStateOf<TextEditorState?>(null) }
var gitMessageEditorState by remember { mutableStateOf<GitMessageEditorState?>(null) }
var interactiveAddState by remember { mutableStateOf<InteractiveAddState?>(null) }
var interactiveRebaseState by remember { mutableStateOf<InteractiveRebaseState?>(null) }
var manPageState by remember { mutableStateOf<ManPageState?>(null) }
var solvedCelebrationTitle by remember { mutableStateOf<String?>(null) }
var showTerminalInputHint by remember { mutableStateOf(true) }
@@ -209,6 +211,8 @@ fun GitHugApp() {
historyDraft = ""
editorState = null
gitMessageEditorState = null
interactiveAddState = null
interactiveRebaseState = null
manPageState = null
applyRecommendedPaneWeights(persist = false)
}
@@ -227,6 +231,8 @@ fun GitHugApp() {
historyDraft = ""
editorState = null
gitMessageEditorState = null
interactiveAddState = null
interactiveRebaseState = null
manPageState = null
paneLayout = paneLayout.copy(
weights = paneLayout.weights + recommendedPaneWeights(
@@ -418,6 +424,59 @@ fun GitHugApp() {
applyCommandResult(state.invocation.command, newRepo, lines, echoCommand = false)
}
fun openInteractiveAdd(invocation: InteractiveAddInvocation) {
output = buildList {
addAll(output)
add("$ ${invocation.command}")
add("Opened interactive add")
}
interactiveAddState = InteractiveAddState(
invocation = invocation,
files = repo.files,
)
applyRecommendedPaneWeights(persist = false)
}
fun stageInteractiveSelection(selectedFiles: Set<String>) {
val state = interactiveAddState ?: return
val updatedRepo = repo.copy(
files = repo.files.map { file ->
if (file.name in selectedFiles) file.copy(staged = true) else file
},
)
interactiveAddState = null
val lines = if (selectedFiles.isEmpty()) {
listOf("No changes staged")
} else {
listOf("staged ${selectedFiles.size} path(s)") + selectedFiles.sorted().map { " $it" }
}
applyCommandResult(state.invocation.command, updatedRepo, lines, echoCommand = false)
}
fun openInteractiveRebase(invocation: InteractiveRebaseInvocation) {
val count = invocation.commitCount ?: repo.commits.size
val commits = repo.commits.takeLast(count.coerceAtLeast(0))
output = buildList {
addAll(output)
add("$ ${invocation.command}")
add("Opened interactive rebase")
}
interactiveRebaseState = InteractiveRebaseState(invocation = invocation, commits = commits)
applyRecommendedPaneWeights(persist = false)
}
fun applyInteractiveRebaseSelection(drafts: List<RebaseCommitDraft>) {
val state = interactiveRebaseState ?: return
interactiveRebaseState = null
val newRepo = applyInteractiveRebase(repo, drafts)
applyCommandResult(
raw = state.invocation.command,
newRepo = newRepo,
lines = listOf("Successfully rebased and updated ${repo.headBranch}."),
echoCommand = false,
)
}
fun runCommand() {
val submittedText = commandInput.text
val raw = submittedText.trim()
@@ -453,6 +512,18 @@ fun GitHugApp() {
return
}
val interactiveAddInvocation = parseInteractiveAddInvocation(raw)
if (interactiveAddInvocation != null) {
openInteractiveAdd(interactiveAddInvocation)
return
}
val interactiveRebaseInvocation = parseInteractiveRebaseInvocation(raw)
if (interactiveRebaseInvocation != null) {
openInteractiveRebase(interactiveRebaseInvocation)
return
}
val (newRepo, lines) = runtime.execute(currentLevel, repo, raw)
applyCommandResult(raw, newRepo, lines, echoCommand = true)
}
@@ -615,6 +686,28 @@ fun GitHugApp() {
)
}
interactiveAddState?.let { state ->
InteractiveAddDialog(
state = state,
onClose = {
output = output + "Interactive add closed without staging"
interactiveAddState = null
},
onStage = { selectedFiles -> stageInteractiveSelection(selectedFiles) },
)
}
interactiveRebaseState?.let { state ->
InteractiveRebaseDialog(
state = state,
onClose = {
output = output + "Interactive rebase closed without applying"
interactiveRebaseState = null
},
onApply = { drafts -> applyInteractiveRebaseSelection(drafts) },
)
}
manPageState?.let { state ->
ManPageDialog(
state = state,

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
@@ -31,6 +32,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
data class GitMessageEditorState(
val invocation: GitEditorInvocation,
@@ -46,13 +48,23 @@ fun GitMessageEditorDialog(
) {
val horizontalScroll = rememberScrollState()
val verticalScroll = rememberScrollState()
val dialogScroll = rememberScrollState()
Dialog(onDismissRequest = onClose) {
Dialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Box(
modifier = Modifier
.fillMaxSize()
.imePadding()
.verticalScroll(dialogScroll)
.padding(12.dp),
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.72f)
.imePadding(),
.heightIn(min = 320.dp, max = 620.dp),
color = PanelPrimary,
shape = RoundedCornerShape(8.dp),
) {
@@ -109,6 +121,7 @@ fun GitMessageEditorDialog(
}
}
}
}
@Composable
private fun EditorButton(

View File

@@ -0,0 +1,21 @@
package solutions.tretter.githugandroid
data class InteractiveAddInvocation(
val command: String,
val pathspec: String?,
)
fun parseInteractiveAddInvocation(command: String): InteractiveAddInvocation? {
val tokens = GitSandboxEngine.tokenizeCommand(command)
if (tokens.size < 3 || tokens.firstOrNull() != "git") return null
val subcommand = tokens.getOrNull(1)
if (subcommand != "add" && subcommand != "stage") return null
if (tokens.drop(2).none { it == "-i" || it == "--interactive" }) return null
val pathspec = tokens
.drop(2)
.firstOrNull { token -> token != "-i" && token != "--interactive" && !token.startsWith("-") }
return InteractiveAddInvocation(command = command, pathspec = pathspec)
}

View File

@@ -0,0 +1,181 @@
package solutions.tretter.githugandroid
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.Surface
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
data class InteractiveAddState(
val invocation: InteractiveAddInvocation,
val files: List<GitFile>,
)
@Composable
fun InteractiveAddDialog(
state: InteractiveAddState,
onClose: () -> Unit,
onStage: (Set<String>) -> Unit,
) {
val candidates = remember(state.files, state.invocation.pathspec) {
state.files
.filter { file -> !file.staged }
.filter { file ->
val pathspec = state.invocation.pathspec
pathspec == null || pathspec == "." || file.name == pathspec || file.name.startsWith(pathspec.trimEnd('/') + "/")
}
}
var selectedFiles by remember(candidates) {
mutableStateOf(candidates.map { it.name }.toSet())
}
val scrollState = rememberScrollState()
Dialog(onDismissRequest = onClose) {
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.76f)
.imePadding(),
color = PanelPrimary,
shape = RoundedCornerShape(8.dp),
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(
text = "Interactive Add",
color = TextPrimary,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
Text(
text = state.invocation.command,
color = TextSecondary,
fontFamily = FontFamily.Monospace,
fontSize = 13.sp,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
DialogButton(label = "Close", onClick = onClose)
DialogButton(
label = "Stage",
enabled = selectedFiles.isNotEmpty(),
onClick = { onStage(selectedFiles) },
)
DialogButton(
label = "All",
enabled = candidates.isNotEmpty(),
onClick = { selectedFiles = candidates.map { it.name }.toSet() },
)
DialogButton(
label = "None",
enabled = selectedFiles.isNotEmpty(),
onClick = { selectedFiles = emptySet() },
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.background(TerminalBackground, RoundedCornerShape(6.dp))
.verticalScroll(scrollState)
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (candidates.isEmpty()) {
Text(
text = "No changes available to stage.",
color = TextSecondary,
fontFamily = FontFamily.Monospace,
)
} else {
candidates.forEach { file ->
val checked = file.name in selectedFiles
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = checked,
onCheckedChange = { isChecked ->
selectedFiles = if (isChecked) {
selectedFiles + file.name
} else {
selectedFiles - file.name
}
},
colors = CheckboxDefaults.colors(
checkedColor = Accent,
uncheckedColor = TextMuted,
checkmarkColor = AppBackground,
),
)
Text(
text = "${file.statusLabel()} ${file.name}",
color = TextPrimary,
fontFamily = FontFamily.Monospace,
fontSize = 13.sp,
)
}
}
}
}
}
}
}
}
private fun GitFile.statusLabel(): String {
return when {
deleted -> "deleted: "
tracked -> "modified: "
else -> "new file: "
}
}
@Composable
private fun DialogButton(
label: String,
enabled: Boolean = true,
onClick: () -> Unit,
) {
Button(
onClick = onClick,
enabled = enabled,
colors = ButtonDefaults.buttonColors(
containerColor = Accent,
contentColor = AppBackground,
disabledContainerColor = PanelTertiary,
disabledContentColor = TextMuted,
),
) {
Text(label)
}
}

View File

@@ -0,0 +1,25 @@
package solutions.tretter.githugandroid
data class InteractiveRebaseInvocation(
val command: String,
val commitCount: Int?,
)
fun parseInteractiveRebaseInvocation(command: String): InteractiveRebaseInvocation? {
val tokens = GitSandboxEngine.tokenizeCommand(command)
if (tokens.size < 3 || tokens.firstOrNull() != "git") return null
if (tokens.getOrNull(1) != "rebase") return null
if (tokens.drop(2).none { it == "-i" || it == "--interactive" }) return null
val commitCount = tokens
.drop(2)
.firstNotNullOfOrNull { token ->
Regex("""HEAD[~^](\d+)""")
.matchEntire(token)
?.groupValues
?.getOrNull(1)
?.toIntOrNull()
}
return InteractiveRebaseInvocation(command = command, commitCount = commitCount)
}

View File

@@ -0,0 +1,238 @@
package solutions.tretter.githugandroid
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
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.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
data class InteractiveRebaseState(
val invocation: InteractiveRebaseInvocation,
val commits: List<CommitNode>,
)
enum class RebaseAction(val label: String) {
PICK("pick"),
SQUASH("squash"),
DROP("drop"),
}
data class RebaseCommitDraft(
val commit: CommitNode,
val message: String,
val action: RebaseAction,
)
@Composable
fun InteractiveRebaseDialog(
state: InteractiveRebaseState,
onClose: () -> Unit,
onApply: (List<RebaseCommitDraft>) -> Unit,
) {
val scrollState = rememberScrollState()
var drafts by remember(state.commits) {
mutableStateOf(state.commits.map { commit ->
RebaseCommitDraft(
commit = commit,
message = if (commit.message == "First coommit") "First commit" else commit.message,
action = if (commit.message.contains("squash this commit", ignoreCase = true)) {
RebaseAction.SQUASH
} else {
RebaseAction.PICK
},
)
}.withSuggestedOrder())
}
Dialog(onDismissRequest = onClose) {
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.84f)
.imePadding(),
color = PanelPrimary,
shape = RoundedCornerShape(8.dp),
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(
text = "Interactive Rebase",
color = TextPrimary,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
Text(
text = state.invocation.command,
color = TextSecondary,
fontFamily = FontFamily.Monospace,
fontSize = 13.sp,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
RebaseButton(label = "Close", onClick = onClose)
RebaseButton(label = "Apply", onClick = { onApply(drafts) })
}
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.background(TerminalBackground, RoundedCornerShape(6.dp))
.verticalScroll(scrollState)
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
if (drafts.isEmpty()) {
Text(
text = "No commits available for interactive rebase.",
color = TextSecondary,
fontFamily = FontFamily.Monospace,
)
}
drafts.forEachIndexed { index, draft ->
Column(
modifier = Modifier
.fillMaxWidth()
.background(PanelSecondary, RoundedCornerShape(6.dp))
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Text(
text = "${draft.action.label} ${draft.commit.id}",
color = Accent,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
)
OutlinedTextField(
value = draft.message,
onValueChange = { message ->
drafts = drafts.replaceAt(index, draft.copy(message = message))
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
)
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
RebaseButton(
label = "Pick",
enabled = draft.action != RebaseAction.PICK,
onClick = { drafts = drafts.replaceAt(index, draft.copy(action = RebaseAction.PICK)) },
)
RebaseButton(
label = "Squash",
enabled = draft.action != RebaseAction.SQUASH && index > 0,
onClick = { drafts = drafts.replaceAt(index, draft.copy(action = RebaseAction.SQUASH)) },
)
RebaseButton(
label = "Drop",
enabled = draft.action != RebaseAction.DROP,
onClick = { drafts = drafts.replaceAt(index, draft.copy(action = RebaseAction.DROP)) },
)
}
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
RebaseButton(
label = "Up",
enabled = index > 0,
onClick = { drafts = drafts.move(index, index - 1) },
)
RebaseButton(
label = "Down",
enabled = index < drafts.lastIndex,
onClick = { drafts = drafts.move(index, index + 1) },
)
}
}
}
}
}
}
}
}
fun applyInteractiveRebase(repo: RepoState, drafts: List<RebaseCommitDraft>): RepoState {
val rebasedIds = drafts.map { it.commit.id }.toSet()
val untouched = repo.commits.filterNot { it.id in rebasedIds }
val rebased = drafts.fold(emptyList<CommitNode>()) { commits, draft ->
when (draft.action) {
RebaseAction.PICK -> commits + draft.commit.copy(message = draft.message)
RebaseAction.DROP -> commits
RebaseAction.SQUASH -> commits
}
}
val nextCommits = untouched + rebased
return repo.copy(
commits = nextCommits,
branches = repo.branches + (repo.headBranch to nextCommits.size),
)
}
private fun List<RebaseCommitDraft>.replaceAt(index: Int, draft: RebaseCommitDraft): List<RebaseCommitDraft> {
return mapIndexed { currentIndex, currentDraft -> if (currentIndex == index) draft else currentDraft }
}
private fun List<RebaseCommitDraft>.move(from: Int, to: Int): List<RebaseCommitDraft> {
return toMutableList().also { drafts ->
val item = drafts.removeAt(from)
drafts.add(to, item)
}
}
private fun List<RebaseCommitDraft>.withSuggestedOrder(): List<RebaseCommitDraft> {
val messages = map { it.message }
if (!messages.containsAll(listOf("First commit", "Second commit", "Third commit"))) return this
return sortedBy { draft ->
when (draft.message) {
"First commit" -> 1
"Second commit" -> 2
"Third commit" -> 3
else -> 0
}
}
}
@Composable
private fun RebaseButton(
label: String,
enabled: Boolean = true,
onClick: () -> Unit,
) {
Button(
onClick = onClick,
enabled = enabled,
colors = ButtonDefaults.buttonColors(
containerColor = Accent,
contentColor = AppBackground,
disabledContainerColor = PanelTertiary,
disabledContentColor = TextMuted,
),
) {
Text(label)
}
}

View File

@@ -30,9 +30,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntSize
@@ -59,8 +59,8 @@ fun ManPageDialog(
) {
val horizontalScrollState = rememberScrollState()
val verticalScrollState = rememberScrollState()
val density = LocalDensity.current
var contentViewportSize by remember { mutableStateOf(IntSize.Zero) }
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
var searchQuery by remember(state.content) { mutableStateOf("") }
var selectedMatch by remember(state.content) { mutableStateOf(0) }
val contentLines = remember(state.content) { state.content.lines() }
@@ -110,18 +110,21 @@ fun ManPageDialog(
selectedMatch = selectedMatch.coerceIn(0, (matchCount - 1).coerceAtLeast(0))
}
LaunchedEffect(selectedMatch, matches) {
LaunchedEffect(selectedMatch, matches, textLayoutResult, contentViewportSize) {
val match = matches.getOrNull(selectedMatch) ?: return@LaunchedEffect
val lineHeightPx = with(density) { 18.sp.toPx() }
val charWidthPx = with(density) { 8.sp.toPx() }
val layoutResult = textLayoutResult ?: return@LaunchedEffect
val startBounds = layoutResult.getBoundingBox(match.start)
val endBounds = layoutResult.getBoundingBox((match.end - 1).coerceAtLeast(match.start))
val viewportHeight = contentViewportSize.height.takeIf { it > 0 } ?: 0
val viewportWidth = contentViewportSize.width.takeIf { it > 0 } ?: 0
val highlightCenterY = (startBounds.top + endBounds.bottom) / 2f
val highlightCenterX = (startBounds.left + endBounds.right) / 2f
val verticalTarget = (
match.lineIndex * lineHeightPx - viewportHeight / 2f + lineHeightPx / 2f
highlightCenterY - viewportHeight / 2f
).toInt()
.coerceIn(0, verticalScrollState.maxValue)
val horizontalTarget = (
match.columnIndex * charWidthPx - viewportWidth / 2f + (match.end - match.start) * charWidthPx / 2f
highlightCenterX - viewportWidth / 2f
).toInt()
.coerceIn(0, horizontalScrollState.maxValue)
verticalScrollState.animateScrollTo(verticalTarget)
@@ -233,6 +236,7 @@ fun ManPageDialog(
Text(
text = highlightedContent,
modifier = Modifier.widthIn(min = 1200.dp),
onTextLayout = { textLayoutResult = it },
color = TextPrimary,
fontFamily = FontFamily.Monospace,
fontSize = 13.sp,

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
@@ -33,6 +34,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.foundation.text.KeyboardOptions
data class TextEditorState(
@@ -53,13 +55,23 @@ fun TextEditorDialog(
) {
val editorHorizontalScroll = rememberScrollState()
val editorVerticalScroll = rememberScrollState()
val dialogScroll = rememberScrollState()
Dialog(onDismissRequest = onClose) {
Dialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Box(
modifier = Modifier
.fillMaxSize()
.imePadding()
.verticalScroll(dialogScroll)
.padding(12.dp),
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.86f)
.imePadding(),
.heightIn(min = 360.dp, max = 720.dp),
color = PanelPrimary,
shape = RoundedCornerShape(8.dp),
) {
@@ -127,6 +139,7 @@ fun TextEditorDialog(
}
}
}
}
@Composable
private fun EditorButton(