diff --git a/app/src/main/java/com/astralquarks/notes/ui/components/InteractiveMarkdownEditor.kt b/app/src/main/java/com/astralquarks/notes/ui/components/InteractiveMarkdownEditor.kt index 73e0051..92f07ad 100644 --- a/app/src/main/java/com/astralquarks/notes/ui/components/InteractiveMarkdownEditor.kt +++ b/app/src/main/java/com/astralquarks/notes/ui/components/InteractiveMarkdownEditor.kt @@ -6,6 +6,13 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.widget.Toast +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.rememberCoroutineScope +import kotlinx.coroutines.launch import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -83,6 +90,7 @@ import coil.compose.AsyncImage * Interactive Live Markdown Editor with immediate real-time synchronization, * auto-list continuation, instant in-place checklist item toggling, and rich preview. */ +@OptIn(ExperimentalFoundationApi::class) @Composable fun InteractiveMarkdownEditor( value: TextFieldValue, @@ -93,6 +101,9 @@ fun InteractiveMarkdownEditor( ) { val context = LocalContext.current var showInteractiveChecklistOverlay by remember { mutableStateOf(false) } + val bringIntoViewRequester = remember { BringIntoViewRequester() } + val coroutineScope = rememberCoroutineScope() + // Check if the current note has checklist items val hasChecklistItems = remember(value.text) { @@ -315,6 +326,15 @@ fun InteractiveMarkdownEditor( } } + var textLayoutResult by remember { mutableStateOf(null) } + + LaunchedEffect(value.selection) { + val cursorRect = textLayoutResult?.getCursorRect(value.selection.start) + if (cursorRect != null) { + bringIntoViewRequester.bringIntoView(cursorRect) + } + } + // Live Markdown Input Field with Instant Synchronous Save & Auto-Continuations BasicTextField( value = value, @@ -323,6 +343,9 @@ fun InteractiveMarkdownEditor( val handled = MarkdownAutoListHelper.handleTextChange(value, incoming) onValueChange(handled) }, + onTextLayout = { result -> + textLayoutResult = result + }, textStyle = TextStyle( fontSize = 16.sp, lineHeight = 25.sp, @@ -350,7 +373,9 @@ fun InteractiveMarkdownEditor( innerTextField() } }, + modifier = Modifier + .bringIntoViewRequester(bringIntoViewRequester) .fillMaxWidth() .testTag("note_content_input") ) diff --git a/app/src/main/java/com/astralquarks/notes/ui/screens/NoteEditScreen.kt b/app/src/main/java/com/astralquarks/notes/ui/screens/NoteEditScreen.kt index 964dd19..331f4a3 100644 --- a/app/src/main/java/com/astralquarks/notes/ui/screens/NoteEditScreen.kt +++ b/app/src/main/java/com/astralquarks/notes/ui/screens/NoteEditScreen.kt @@ -63,6 +63,11 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.InputChip import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SmallFloatingActionButton +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.runtime.rememberCoroutineScope +import kotlinx.coroutines.launch import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -213,6 +218,9 @@ fun NoteEditScreen( val textColor = remember(backgroundColor) { NoteColorPalette.getNoteTextColor(backgroundColor) } val secondaryTextColor = remember(backgroundColor) { NoteColorPalette.getNoteSecondaryTextColor(backgroundColor) } + val scrollState = rememberScrollState() + val coroutineScope = rememberCoroutineScope() + Scaffold( modifier = modifier .fillMaxSize() @@ -350,6 +358,38 @@ fun NoteEditScreen( ) ) }, + floatingActionButton = { + if (isRawMode) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(bottom = 60.dp) // padding above the toolbar + ) { + SmallFloatingActionButton( + onClick = { + coroutineScope.launch { + scrollState.animateScrollTo(0) + } + }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer + ) { + Icon(Icons.Default.KeyboardArrowUp, contentDescription = "Scroll to Top") + } + SmallFloatingActionButton( + onClick = { + coroutineScope.launch { + scrollState.animateScrollTo(scrollState.maxValue) + } + }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer + ) { + Icon(Icons.Default.KeyboardArrowDown, contentDescription = "Scroll to Bottom") + } + } + } + }, bottomBar = { if (isRawMode) { Box( @@ -390,7 +430,7 @@ fun NoteEditScreen( .fillMaxSize() .padding(innerPadding) .padding(horizontal = 16.dp) - .verticalScroll(rememberScrollState()), + .verticalScroll(scrollState), verticalArrangement = Arrangement.spacedBy(10.dp) ) { // Note Title Input