diff --git a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/AppTextEditor.kt b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/AppTextEditor.kt index 96e9c6e1a..798f7574c 100644 --- a/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/AppTextEditor.kt +++ b/application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/ui/desktop/AppTextEditor.kt @@ -38,6 +38,8 @@ fun AppTextEditor( listState: LazyListState = rememberLazyListState(), modifier: Modifier = Modifier ) { + println("draw!!") + LaunchedEffect(Unit) { manager.scrollToPosition.collectLatest { position -> if (position == -1) { diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/HeaderDrawer.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/HeaderDrawer.kt index ddc61b153..ac3b291bc 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/HeaderDrawer.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/HeaderDrawer.kt @@ -195,6 +195,7 @@ fun headerDrawer( enabled = enabled, isDarkTheme = isDarkTheme, onTextEdit = manager::handleTextInput, + onSelectionChange = manager::onTextSelectionChanged, onKeyEvent = onKeyEvent, lineBreakByContent = lineBreakByContent, emptyErase = EmptyErase.DISABLED, diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt index ed0b4bb15..c96376d18 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt @@ -82,6 +82,7 @@ class TextDrawer( { _, _, _, _, _, _, _ -> false }, private val textStyle: @Composable (StoryStep) -> TextStyle = { defaultTextStyle(it) }, private val onTextEdit: (TextInput, Double, Boolean) -> Unit = { _, _, _ -> }, + private val onSelectionChange: (position: Double, start: Int, end: Int) -> Unit = { _, _, _ -> }, private val lineBreakByContent: Boolean = true, private val enabled: Boolean = true, private val emptyErase: EmptyErase = EmptyErase.CHANGE_TYPE, @@ -259,11 +260,15 @@ class TextDrawer( val previousStart = inputText.selection.start val sizeDifference = value.text.length - inputText.text.length + val textChanged = value.text != inputText.text if (abs(sizeDifference) > 0) { spans = Spans.recalculateSpans(spans, previousStart, sizeDifference) } + // Report selection change for formatting operations (without triggering full redraw) + onSelectionChange(drawInfo.position, start, end) + // Detect slash command if (slashCommandsEnabled) { val text = value.text @@ -321,7 +326,8 @@ class TextDrawer( ) } - if (!showSlashCommandPopup) { + // Only call onTextEdit when text actually changes, not on selection-only changes + if (!showSlashCommandPopup && textChanged) { onTextEdit( TextInput(value.text, start, end, spans), drawInfo.position, @@ -490,6 +496,7 @@ fun DesktopMessageDrawerPreview() { aiExplanation = "", selectionState = MutableStateFlow(false), onSelectionLister = {}, + onSelectionChange = { _, _, _ -> }, ).Text( step = StoryStep(text = "Some text", type = StoryTypes.TEXT.type), drawInfo = DrawInfo(), diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt index 4983c4352..9096aab56 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/factory/CommonDrawers.kt @@ -342,6 +342,7 @@ private fun RowScope.messageDrawer( modifier = modifier.weight(1F), onKeyEvent = eventListener, onTextEdit = manager::handleTextInput, + onSelectionChange = manager::onTextSelectionChanged, textStyle = textStyle, onFocusChanged = { position, focus -> manager.onFocusChange(position, focus.isFocused) diff --git a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt index 2ef2249fc..62e222481 100644 --- a/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt +++ b/writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt @@ -227,6 +227,14 @@ class WriteopiaStateManager( private val _onEditPositions = MutableStateFlow(setOf()) val onEditPositions = _onEditPositions.asStateFlow() + /** + * Separate selection StateFlow for tracking text selection without triggering toDraw redraws. + * This is used for formatting operations (bold, italic, etc.) without causing all text + * drawers to recompose. + */ + private val _textSelection = MutableStateFlow(Selection.start()) + val textSelection: StateFlow = _textSelection.asStateFlow() + /** * Tracks the anchor position for keyboard-based multi-line selection. * This is the position from which the selection extends when using shift+arrows. @@ -238,10 +246,7 @@ class WriteopiaStateManager( val currentStory: StateFlow = _currentStory.asStateFlow() - val textSelectionState: Flow = - _currentStory.map { storyState -> - storyState.selection - } + val textSelectionState: Flow = _textSelection.asStateFlow() val currentDocument: StateFlow = combine(_documentInfo, _currentStory) { info, state -> @@ -276,7 +281,7 @@ class WriteopiaStateManager( .mapValues { (position, storyStep) -> DrawStory( storyStep = storyStep, - cursor = storyState.selection.takeIf { it.position == position }, + cursor = null, // No longer pass cursor here to avoid unnecessary redraws isSelected = positions.contains(position), position = position ) @@ -291,11 +296,11 @@ class WriteopiaStateManager( private var initialized = false val selectionMetadataState: Flow> = - combine(_currentStory, _onEditPositions) { storyState, onEditPositions -> + combine(_currentStory, _onEditPositions, _textSelection) { storyState, onEditPositions, selection -> val result = mutableSetOf() val findMetadata = { storyStep: StoryStep -> - val (selectStart, selectEnd) = (storyState.selection.start to storyState.selection.end) + val (selectStart, selectEnd) = (selection.start to selection.end) val fromType = SelectionMetadata.fromStoryType(storyStep.type.number) if (fromType != null) { @@ -336,7 +341,7 @@ class WriteopiaStateManager( } if (!onEditPositions.isNotEmpty()) { - val selectionPos = storyState.selection.position + val selectionPos = selection.position val step = storyState.stories[selectionPos] if (step != null) { @@ -780,7 +785,9 @@ class WriteopiaStateManager( writeopiaManager.onLineBreak(lineBreak, expanded).let { (_, newState) -> // Todo: Fix this when the inner position are completed // backStackManager.addAction(BackstackAction.Add(newStory, newPosition)) - _currentStory.value = newState.copy(selection = Selection.start()) + val newSelection = Selection.start() + _textSelection.value = newSelection + _currentStory.value = newState.copy(selection = newSelection) _scrollToPosition.value = -1 } } @@ -795,6 +802,15 @@ class WriteopiaStateManager( _currentStory.value = story.copy(focus = position, lastEdit = LastEdit.Nothing) } + /** + * Updates text selection without triggering StoryState changes. + * This allows selection changes to be tracked for formatting operations + * without causing all text drawers to recompose. + */ + fun onTextSelectionChanged(position: Double, start: Int, end: Int) { + _textSelection.value = Selection(start, end, position) + } + fun scrollToPosition(position: Int) { _scrollToPosition.value = position } @@ -862,14 +878,17 @@ class WriteopiaStateManager( this[lastPosition] = lastContentStory.copyNewLocalId() } val cursor = lastContentStory.text?.length ?: 0 + val newSelection = Selection.fromPosition( + cursorPosition = cursor, + stepPosition = lastPosition + ) + + _textSelection.value = newSelection _currentStory.value.copy( focus = lastPosition, stories = newStoriesState, - selection = Selection.fromPosition( - cursorPosition = cursor, - stepPosition = lastPosition - ) + selection = newSelection ) } else { val newPosition = stories.size.toDouble() @@ -887,15 +906,18 @@ class WriteopiaStateManager( } val cursor = newLastMessage.text?.length ?: 0 + val newSelection = Selection.fromPosition( + cursorPosition = cursor, + stepPosition = newPosition + ) + + _textSelection.value = newSelection StoryState( updatedStories, LastEdit.LineEdition(newPosition, newLastMessage), newPosition, - selection = Selection.fromPosition( - cursorPosition = cursor, - stepPosition = newPosition - ) + selection = newSelection ) } @@ -972,12 +994,12 @@ class WriteopiaStateManager( val state = writeopiaManager.onErase(eraseStory, _currentStory.value).let { state -> if (previousStory != null && newFocus != null) { - state.copy( - selection = Selection.fromPosition( - previousStory.text?.length ?: 0, - newFocus - ) + val newSelection = Selection.fromPosition( + previousStory.text?.length ?: 0, + newFocus ) + _textSelection.value = newSelection + state.copy(selection = newSelection) } else { state } @@ -1081,7 +1103,7 @@ class WriteopiaStateManager( _currentStory.value = writeopiaManager.addSpanToStories(_currentStory.value, onEdit, span) } else { - val selection = currentStory.value.selection + val selection = _textSelection.value val (start, end) = selection.sortedPositions() _currentStory.value = writeopiaManager.addSpan( @@ -1360,7 +1382,7 @@ class WriteopiaStateManager( val maxSelected = _onEditPositions.value.maxOrNull() ?: return null getStory(maxSelected)?.nextPosition } else { - val currentPos = currentStory.value.selection.position + val currentPos = _textSelection.value.position getStory(currentPos)?.nextPosition } @@ -1508,7 +1530,7 @@ class WriteopiaStateManager( } private fun currentLineText(): String? { - val selection = currentStory.value.selection + val selection = _textSelection.value val (start, end) = selection.sortedPositions() return when { @@ -1540,9 +1562,12 @@ class WriteopiaStateManager( mutable[newPosition] = step + val newSelection = Selection.fromLastLine(cursor, newPosition) + _textSelection.value = newSelection + _currentStory.value = storyState.copy( focus = newPosition, - selection = Selection.fromLastLine(cursor, newPosition), + selection = newSelection, stories = mutable ) } @@ -1647,7 +1672,7 @@ class WriteopiaStateManager( lastStateChange = stateChange val currentFocusPosition = _currentStory.value.focus - val currentSelectionPosition = _currentStory.value.selection.position + val currentSelectionPosition = _textSelection.value.position writeopiaManager.changeStoryState(stateChange, _currentStory.value).let { state -> if (trackIt) { @@ -1666,13 +1691,18 @@ class WriteopiaStateManager( state.focus } + val newSelection = Selection( + stateChange.selectionStart ?: state.selection.start, + stateChange.selectionEnd ?: state.selection.end, + newSelectionPosition + ) + + // Update separate selection state for formatting operations + _textSelection.value = newSelection + _currentStory.value = state.copy( focus = newFocus, - selection = Selection( - stateChange.selectionStart ?: state.selection.start, - stateChange.selectionEnd ?: state.selection.end, - newSelectionPosition - ) + selection = newSelection ) } }