Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.foundation.text

import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color

@Composable
internal actual fun platformShouldDrawTextControls(cursorBrush: Brush, selectionColor: Color): Boolean = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.foundation.text.input.internal

import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange

internal actual fun CompositionLocalConsumerModifierNode.drawPlatformSelection(
scope: DrawScope,
selection: TextRange,
textLayoutResult: TextLayoutResult
) = drawDefaultSelection(scope, selection, textLayoutResult)

internal actual fun CompositionLocalConsumerModifierNode.drawPlatformCursor(
scope: DrawScope,
cursorRect: Rect,
brush: Brush,
alpha: Float
) = drawDefaultCursor(scope, cursorRect, brush, alpha)
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ internal fun CoreTextField(
offsetMapping,
)

val platformDrawsTextControls = platformShouldDrawTextControls(cursorBrush, state.selectionBackgroundColor)
val drawModifier =
Modifier.drawBehind {
state.layoutResult?.let { layoutResult ->
Expand All @@ -407,6 +408,7 @@ internal fun CoreTextField(
layoutResult.value,
state.highlightPaint,
state.selectionBackgroundColor,
!platformDrawsTextControls
)
}
}
Expand Down Expand Up @@ -463,7 +465,7 @@ internal fun CoreTextField(
focusRequester,
)

val showCursor = enabled && !readOnly && windowInfo.isWindowFocused && !state.hasHighlight()
val showCursor = enabled && !readOnly && windowInfo.isWindowFocused && !state.hasHighlight() && !platformDrawsTextControls
val cursorModifier = Modifier.cursor(state, value, offsetMapping, cursorBrush, showCursor)

DisposableEffect(manager) { onDispose { manager.hideSelectionToolbar() } }
Expand Down Expand Up @@ -1115,6 +1117,16 @@ internal expect fun CursorHandle(
minTouchTargetSize: DpSize = DpSize.Unspecified,
)

/**
* Determines whether the platform should handle drawing text controls, such as cursor and selection highlights.
*
* @param cursorBrush A brush used to draw the cursor in the text field.
* @param selectionColor The color used to highlight the selected text.
* @return A boolean value indicating whether the platform should handle drawing text controls.
*/
@Composable
internal expect fun platformShouldDrawTextControls(cursorBrush: Brush, selectionColor: Color): Boolean

// TODO(b/262648050) Try to find a better API.
private fun notifyFocusedRect(
state: LegacyTextFieldState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ internal class TextFieldDelegate {
textLayoutResult: TextLayoutResult,
highlightPaint: Paint,
selectionBackgroundColor: Color,
drawSelectionHighlight: Boolean = true,
) {
if (!selectionPreviewHighlightRange.collapsed) {
highlightPaint.color = selectionBackgroundColor
Expand All @@ -157,7 +158,7 @@ internal class TextFieldDelegate {
textLayoutResult,
highlightPaint,
)
} else if (!value.selection.collapsed) {
} else if (!value.selection.collapsed && drawSelectionHighlight) {
highlightPaint.color = selectionBackgroundColor
drawHighlight(
canvas,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,9 +518,7 @@ internal class TextFieldCoreModifierNode(
val start = selection.min
val end = selection.max
if (start != end) {
val selectionBackgroundColor = currentValueOf(LocalTextSelectionColors).backgroundColor
val selectionPath = textLayoutResult.getPathForRange(start, end)
drawPath(selectionPath, color = selectionBackgroundColor)
[email protected](this, selection, textLayoutResult)
}
}

Expand Down Expand Up @@ -571,12 +569,13 @@ internal class TextFieldCoreModifierNode(

val cursorRect = textFieldSelectionState.getCursorRect()

drawLine(
cursorBrush,
cursorRect.topCenter,
cursorRect.bottomCenter,
// Delegate the actual drawing to platform-specific implementation, passing only
// prepared parameters to avoid exposing private members outside this node.
[email protected](
scope = this,
cursorRect = cursorRect,
brush = cursorBrush,
alpha = cursorAlphaValue,
strokeWidth = cursorRect.width,
)
}

Expand Down Expand Up @@ -681,3 +680,58 @@ private fun Float.roundToNext(): Float =
this > 0 -> ceil(this)
else -> floor(this)
}

/**
* Draws the visual highlight for the given text [selection].
*
* Platforms may override this to customize how text selection is rendered. The shared default
* implementation is provided by `drawDefaultSelection`.
*
* @param scope [DrawScope] used for issuing drawing commands.
* @param selection Range of selected text in [textLayoutResult].
* @param textLayoutResult Layout information used to map [selection] to canvas coordinates.
*/
internal expect fun CompositionLocalConsumerModifierNode.drawPlatformSelection(scope: DrawScope, selection: TextRange, textLayoutResult: TextLayoutResult)

internal fun CompositionLocalConsumerModifierNode.drawDefaultSelection(scope: DrawScope, selection: TextRange, textLayoutResult: TextLayoutResult) {
val selectionBackgroundColor = currentValueOf(LocalTextSelectionColors).backgroundColor
val selectionPath = textLayoutResult.getPathForRange(selection.min, selection.max)
with(scope) {
drawPath(selectionPath, color = selectionBackgroundColor)
}
}

/**
* Draws the visual cursor indicator using the provided [cursorRect].
*
* Platforms may override this to customize how the text cursor is rendered. The shared default
* implementation is provided by `drawDefaultCursor`.
*
* @param scope [DrawScope] used for issuing drawing commands.
* @param cursorRect Rectangle representing the cursor in canvas coordinates.
* @param brush [Brush] used to paint the cursor.
* @param alpha Opacity to use when drawing the cursor.
*/
internal expect fun CompositionLocalConsumerModifierNode.drawPlatformCursor(
scope: DrawScope,
cursorRect: Rect,
brush: Brush,
alpha: Float,
)

internal fun CompositionLocalConsumerModifierNode.drawDefaultCursor(
scope: DrawScope,
cursorRect: Rect,
brush: Brush,
alpha: Float,
) {
with(scope) {
drawLine(
brush = brush,
start = cursorRect.topCenter,
end = cursorRect.bottomCenter,
alpha = alpha,
strokeWidth = cursorRect.width,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.UIKitNativeTextInputContext
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.uikit.LocalNativeTextInputContext
import androidx.compose.ui.uikit.utils.CMPEditMenuView
import androidx.compose.ui.uikit.utils.CMPEditMenuCustomAction
import androidx.compose.ui.unit.Density
Expand Down Expand Up @@ -145,6 +147,8 @@ private fun ProvideNewContextMenuDefaultProviders(
) {
val toolbarProvider = LocalTextContextMenuToolbarProvider.current
val dropdownProvider = LocalTextContextMenuDropdownProvider.current
val contextMenuHandlerProvider = LocalNativeTextInputContext.current

if (toolbarProvider == null || dropdownProvider == null) {
val layoutCoordinates: MutableState<LayoutCoordinates?> = remember {
mutableStateOf(null, neverEqualPolicy())
Expand All @@ -160,6 +164,7 @@ private fun ProvideNewContextMenuDefaultProviders(
menuDelay = menuDelay,
editMenuView = editMenuView,
density = density,
nativeContextMenuHandler = contextMenuHandlerProvider,
coordinates = { layoutCoordinates.value }
)
}
Expand Down Expand Up @@ -197,6 +202,7 @@ private class ContextMenuToolbarProvider(
private val menuDelay: Duration,
val editMenuView: CMPEditMenuView,
private val density: Density,
private val nativeContextMenuHandler: UIKitNativeTextInputContext,
private val coordinates: () -> LayoutCoordinates?
): TextContextMenuProvider {
@OptIn(FlowPreview::class)
Expand Down Expand Up @@ -265,8 +271,17 @@ private class ContextMenuToolbarProvider(
rect = rect
)
}.filterNotNull().collect {
getEditMenuView().showEditMenuAtRect(
targetRect = it.rect.toCGRect(density),
// getEditMenuView().showEditMenuAtRect(
// targetRect = it.rect.toCGRect(density),
// copy = it.copy,
// cut = it.cut,
// paste = it.paste,
// selectAll = it.selectAll,
// customActions = it.customActions
// )

nativeContextMenuHandler.updateEditMenuState(
targetRect = it.rect,
copy = it.copy,
cut = it.cut,
paste = it.paste,
Expand All @@ -279,7 +294,7 @@ private class ContextMenuToolbarProvider(
suspendCancellableCoroutine { continuation ->
session = TextContextMenuSessionImpl(editMenuView, continuation)
continuation.invokeOnCancellation {
editMenuView.hideEditMenu()
editMenuView.hideEditMenu()
}
}
job.cancel()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.foundation.text

import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.uikit.LocalNativeTextInputContext

@OptIn(ExperimentalComposeUiApi::class)
@Composable
internal actual fun platformShouldDrawTextControls(cursorBrush: Brush, selectionColor: Color): Boolean {
val nativeInputContext = LocalNativeTextInputContext.current
val isUsingNativeInput = nativeInputContext.usingNativeInput()
if (isUsingNativeInput) {
val controlsColor = (cursorBrush as? SolidColor)
?.value
?.takeIf { it != Color.Unspecified }
?: selectionColor
nativeInputContext.updateTintColor(controlsColor)
nativeInputContext.updateCursorThickness(DefaultCursorThickness)
}
return isUsingNativeInput
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.foundation.text.input.internal

import androidx.compose.foundation.text.DefaultCursorThickness
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.uikit.LocalNativeTextInputContext

@OptIn(ExperimentalComposeUiApi::class)
internal actual fun CompositionLocalConsumerModifierNode.drawPlatformSelection(
scope: DrawScope,
selection: TextRange,
textLayoutResult: TextLayoutResult
) {
val usingNITI = currentValueOf(LocalNativeTextInputContext).usingNativeInput()
// Don't draw selection on iOS when using NITI
if (!usingNITI) {
drawDefaultSelection(scope, selection, textLayoutResult)
}
}

@OptIn(ExperimentalComposeUiApi::class)
internal actual fun CompositionLocalConsumerModifierNode.drawPlatformCursor(
scope: DrawScope,
cursorRect: Rect,
brush: Brush,
alpha: Float
) {
val nativeTextInputContext = currentValueOf(LocalNativeTextInputContext)
// Don't draw selection on iOS when using NITI
if (!nativeTextInputContext.usingNativeInput()) {
drawDefaultCursor(scope, cursorRect, brush, alpha)
} else {
(brush as? SolidColor)
?.value
?.takeIf { it != Color.Unspecified }
?.let { nativeTextInputContext.updateTintColor(it) }
nativeTextInputContext.updateCursorThickness(DefaultCursorThickness)
}
}
Loading
Loading