Skip to content

Commit afe936f

Browse files
author
Leandro Ferreira
committed
Adding text toolbox
1 parent aed6a80 commit afe936f

File tree

5 files changed

+154
-83
lines changed

5 files changed

+154
-83
lines changed

application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorKmpViewModel.kt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,21 @@ class NoteEditorKmpViewModel(
144144
override val showSearchState: StateFlow<Boolean> = _showSearch.asStateFlow()
145145
override val searchText: StateFlow<String> = _searchText.asStateFlow()
146146

147+
private val hasLinesSelection = writeopiaManager.onEditPositions
148+
.map { it.isNotEmpty() }
149+
150+
override val hasSelectedLines: StateFlow<Boolean> =
151+
combine(
152+
hasLinesSelection,
153+
writeopiaManager.textSelectionState
154+
) { hasLines, selection ->
155+
val hasTextSelection = selection.start != selection.end
156+
157+
hasLines || hasTextSelection
158+
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
159+
160+
// val selectionOfText = writeopiaManager.
161+
147162
private val findsOfSearch: Flow<Set<Int>> =
148163
combine(writeopiaManager.documentInfo, searchText) { info, query ->
149164
info.id to query
@@ -157,7 +172,7 @@ class NoteEditorKmpViewModel(
157172
override val isEditable: StateFlow<Boolean> = writeopiaManager
158173
.documentInfo
159174
.map { info -> !info.isLocked }
160-
.stateIn(viewModelScope, started = SharingStarted.Lazily, initialValue = false)
175+
.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(), initialValue = false)
161176

162177
private val _showGlobalMenu = MutableStateFlow(false)
163178
override val showGlobalMenu = _showGlobalMenu.asStateFlow()
@@ -192,7 +207,7 @@ class NoteEditorKmpViewModel(
192207
override val currentTitle by lazy {
193208
writeopiaManager.currentDocument.filterNotNull().map { document ->
194209
document.title
195-
}.stateIn(viewModelScope, started = SharingStarted.Lazily, initialValue = "")
210+
}.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(), initialValue = "")
196211
}
197212

198213
private val _shouldGoToNextScreen = MutableStateFlow(false)
@@ -207,7 +222,7 @@ class NoteEditorKmpViewModel(
207222

208223
else -> EditState.TEXT
209224
}
210-
}.stateIn(viewModelScope, started = SharingStarted.Lazily, initialValue = EditState.TEXT)
225+
}.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(), initialValue = EditState.TEXT)
211226
}
212227

213228
private val story: StateFlow<StoryState> = writeopiaManager.currentStory
@@ -229,7 +244,7 @@ class NoteEditorKmpViewModel(
229244
override val notFavorite: StateFlow<Boolean> = writeopiaManager
230245
.documentInfo
231246
.map { info -> !info.isFavorite }
232-
.stateIn(viewModelScope, started = SharingStarted.Lazily, initialValue = false)
247+
.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(), initialValue = false)
233248

234249
private var aiJob: Job? = null
235250

application/features/editor/src/commonMain/kotlin/io/writeopia/editor/features/editor/viewmodel/NoteEditorViewModel.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ interface NoteEditorViewModel : BackstackInform, BackstackHandler {
5050

5151
val searchText: StateFlow<String>
5252

53+
val hasSelectedLines: StateFlow<Boolean>
54+
5355
fun showSearch()
5456

5557
fun hideSearch()

writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/components/EditionScreen.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ fun EditionScreen(
5151

5252
Row(
5353
modifier = Modifier
54-
.weight(1f)
5554
.horizontalScroll(rememberScrollState()),
5655
verticalAlignment = Alignment.CenterVertically
5756
) {

writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/drawer/content/TextDrawer.kt

Lines changed: 127 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
package io.writeopia.ui.drawer.content
22

3+
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.animation.core.Spring
5+
import androidx.compose.animation.core.spring
6+
import androidx.compose.animation.core.tween
7+
import androidx.compose.animation.expandIn
8+
import androidx.compose.animation.fadeIn
9+
import androidx.compose.animation.fadeOut
10+
import androidx.compose.animation.shrinkOut
11+
import androidx.compose.animation.slideIn
12+
import androidx.compose.animation.slideOut
13+
import androidx.compose.foundation.background
314
import androidx.compose.foundation.clickable
415
import androidx.compose.foundation.interaction.MutableInteractionSource
516
import androidx.compose.foundation.layout.Arrangement
17+
import androidx.compose.foundation.layout.Box
618
import androidx.compose.foundation.layout.Row
19+
import androidx.compose.foundation.layout.padding
720
import androidx.compose.foundation.text.BasicTextField
821
import androidx.compose.foundation.text.KeyboardOptions
922
import androidx.compose.foundation.text.input.TextFieldState
23+
import androidx.compose.material3.DropdownMenu
1024
import androidx.compose.material3.MaterialTheme
1125
import androidx.compose.runtime.Composable
1226
import androidx.compose.runtime.LaunchedEffect
@@ -17,7 +31,9 @@ import androidx.compose.runtime.mutableStateOf
1731
import androidx.compose.runtime.remember
1832
import androidx.compose.runtime.rememberCoroutineScope
1933
import androidx.compose.runtime.setValue
34+
import androidx.compose.ui.Alignment
2035
import androidx.compose.ui.Modifier
36+
import androidx.compose.ui.draw.clip
2137
import androidx.compose.ui.focus.FocusRequester
2238
import androidx.compose.ui.focus.FocusState
2339
import androidx.compose.ui.focus.focusRequester
@@ -32,10 +48,15 @@ import androidx.compose.ui.text.TextStyle
3248
import androidx.compose.ui.text.font.FontStyle
3349
import androidx.compose.ui.text.input.KeyboardCapitalization
3450
import androidx.compose.ui.text.input.TextFieldValue
51+
import androidx.compose.ui.unit.IntOffset
52+
import androidx.compose.ui.unit.IntSize
53+
import androidx.compose.ui.unit.dp
54+
import androidx.compose.ui.window.Popup
3555
import io.writeopia.sdk.models.story.StoryStep
3656
import io.writeopia.sdk.models.story.StoryTypes
3757
import io.writeopia.sdk.models.story.Tag
3858
import io.writeopia.sdk.models.story.TagInfo
59+
import io.writeopia.ui.components.EditionScreen
3960
import io.writeopia.ui.drawer.SimpleTextDrawer
4061
import io.writeopia.ui.drawer.factory.EndOfText
4162
import io.writeopia.ui.extensions.toTextRange
@@ -106,6 +127,15 @@ class TextDrawer(
106127
textLayoutResult?.getLineForOffset(inputText.selection.end)
107128
}
108129
}
130+
131+
val selection by remember {
132+
derivedStateOf {
133+
inputText.selection
134+
}
135+
}
136+
137+
val hasSelection = selection.start != selection.end
138+
109139
val realPosition by remember {
110140
derivedStateOf {
111141
val lineStart = textLayoutResult?.multiParagraph?.getLineStart(cursorLine ?: 0)
@@ -138,95 +168,114 @@ class TextDrawer(
138168

139169
val coroutineScope = rememberCoroutineScope()
140170

141-
Row(horizontalArrangement = Arrangement.Center) {
142-
if (isSuggestion) {
171+
Box {
172+
Row(horizontalArrangement = Arrangement.Center) {
173+
if (isSuggestion) {
174+
BasicTextField(
175+
state = TextFieldState(aiExplanation),
176+
textStyle = textStyle(step).copy(fontStyle = FontStyle.Normal)
177+
)
178+
}
179+
143180
BasicTextField(
144-
state = TextFieldState(aiExplanation),
145-
textStyle = textStyle(step).copy(fontStyle = FontStyle.Normal)
146-
)
147-
}
181+
modifier = modifier
182+
.let { modifierLet ->
183+
if (focusRequester != null) {
184+
modifierLet.focusRequester(focusRequester)
185+
} else {
186+
modifierLet
187+
}
188+
}
189+
.onPreviewKeyEvent { keyEvent ->
190+
onKeyEvent(
191+
keyEvent,
192+
inputText,
193+
step,
194+
drawInfo.position,
195+
emptyErase,
196+
realPosition,
197+
isInLastLine
198+
)
199+
}
200+
.onFocusChanged { focusState ->
201+
onFocusChanged(drawInfo.position, focusState)
202+
}
203+
.testTag("MessageDrawer_${drawInfo.position}")
204+
.let { modifierLet ->
205+
if (selectionState) {
206+
modifierLet.clickable { onSelectionLister(drawInfo.position) }
207+
} else {
208+
modifierLet
209+
}
210+
},
211+
value = inputText,
212+
enabled = !selectionState && !drawInfo.selectMode && enabled,
213+
onTextLayout = {
214+
textLayoutResult = it
215+
},
216+
onValueChange = { value ->
217+
val start = value.selection.start
218+
val end = value.selection.end
219+
val previousStart = inputText.selection.start
148220

149-
BasicTextField(
150-
modifier = modifier
151-
.let { modifierLet ->
152-
if (focusRequester != null) {
153-
modifierLet.focusRequester(focusRequester)
154-
} else {
155-
modifierLet
221+
val sizeDifference = value.text.length - inputText.text.length
222+
223+
if (abs(sizeDifference) > 0) {
224+
spans = Spans.recalculateSpans(spans, previousStart, sizeDifference)
156225
}
157-
}
158-
.onPreviewKeyEvent { keyEvent ->
159-
onKeyEvent(
160-
keyEvent,
161-
inputText,
162-
step,
226+
227+
val edit = {
228+
inputText = value.copy(
229+
Spans.createStringWithSpans(
230+
value.text.replace("\n", ""),
231+
spans,
232+
isDarkTheme
233+
)
234+
)
235+
}
236+
237+
onTextEdit(
238+
TextInput(value.text, start, end, spans),
163239
drawInfo.position,
164-
emptyErase,
165-
realPosition,
166-
isInLastLine
240+
lineBreakByContent,
167241
)
168-
}
169-
.onFocusChanged { focusState ->
170-
onFocusChanged(drawInfo.position, focusState)
171-
}
172-
.testTag("MessageDrawer_${drawInfo.position}")
173-
.let { modifierLet ->
174-
if (selectionState) {
175-
modifierLet.clickable { onSelectionLister(drawInfo.position) }
242+
243+
if (start == 0 || end == 0) {
244+
coroutineScope.launch {
245+
// Delay to avoid jumping to previous line too soon when erasing text
246+
delay(70)
247+
edit()
248+
}
176249
} else {
177-
modifierLet
250+
edit()
178251
}
179252
},
180-
value = inputText,
181-
enabled = !selectionState && !drawInfo.selectMode && enabled,
182-
onTextLayout = {
183-
textLayoutResult = it
184-
},
185-
onValueChange = { value ->
186-
val start = value.selection.start
187-
val end = value.selection.end
188-
val previousStart = inputText.selection.start
189-
190-
val sizeDifference = value.text.length - inputText.text.length
191-
192-
if (abs(sizeDifference) > 0) {
193-
spans = Spans.recalculateSpans(spans, previousStart, sizeDifference)
194-
}
195-
196-
val edit = {
197-
inputText = value.copy(
198-
Spans.createStringWithSpans(
199-
value.text.replace("\n", ""),
200-
spans,
201-
isDarkTheme
202-
)
203-
)
204-
}
253+
keyboardOptions = KeyboardOptions(
254+
capitalization = KeyboardCapitalization.Sentences
255+
),
256+
textStyle = textStyle(step),
257+
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
258+
interactionSource = interactionSource,
259+
decorationBox = decorationBox,
260+
)
261+
}
205262

206-
onTextEdit(
207-
TextInput(value.text, start, end, spans),
208-
drawInfo.position,
209-
lineBreakByContent,
263+
264+
Popup(offset = IntOffset(0, -70)) {
265+
AnimatedVisibility(
266+
visible = hasSelection,
267+
enter = fadeIn(animationSpec = tween(durationMillis = 150)),
268+
exit = fadeOut(animationSpec = tween(durationMillis = 150))
269+
) {
270+
EditionScreen(
271+
modifier = Modifier
272+
.padding(bottom = 20.dp)
273+
.clip(MaterialTheme.shapes.large)
274+
.background(MaterialTheme.colorScheme.primary)
210275
)
276+
}
211277

212-
if (start == 0 || end == 0) {
213-
coroutineScope.launch {
214-
// Delay to avoid jumping to previous line too soon when erasing text
215-
delay(70)
216-
edit()
217-
}
218-
} else {
219-
edit()
220-
}
221-
},
222-
keyboardOptions = KeyboardOptions(
223-
capitalization = KeyboardCapitalization.Sentences
224-
),
225-
textStyle = textStyle(step),
226-
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
227-
interactionSource = interactionSource,
228-
decorationBox = decorationBox,
229-
)
278+
}
230279
}
231280
}
232281
}

writeopia_ui/src/commonMain/kotlin/io/writeopia/ui/manager/WriteopiaStateManager.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import kotlinx.coroutines.flow.StateFlow
5151
import kotlinx.coroutines.flow.asStateFlow
5252
import kotlinx.coroutines.flow.combine
5353
import kotlinx.coroutines.flow.filterNotNull
54+
import kotlinx.coroutines.flow.map
5455
import kotlinx.coroutines.flow.onEach
5556
import kotlinx.coroutines.flow.stateIn
5657
import kotlinx.coroutines.launch
@@ -208,6 +209,11 @@ class WriteopiaStateManager(
208209

209210
val currentStory: StateFlow<StoryState> = _currentStory.asStateFlow()
210211

212+
val textSelectionState: Flow<Selection> =
213+
_currentStory.map { storyState ->
214+
storyState.selection
215+
}
216+
211217
val currentDocument: StateFlow<Document?> =
212218
combine(_documentInfo, _currentStory) { info, state ->
213219
parseDocument(info, state)

0 commit comments

Comments
 (0)