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 @@ -387,6 +387,7 @@ internal fun CoreTextField(
manager, enabled, interactionSource, state, focusRequester, readOnly, offsetMapping
)

val platformDrawsTextControls = platformShouldDrawTextControls(cursorBrush, state.selectionBackgroundColor)
val drawModifier =
Modifier.drawBehind {
state.layoutResult?.let { layoutResult ->
Expand All @@ -400,6 +401,7 @@ internal fun CoreTextField(
layoutResult.value,
state.highlightPaint,
state.selectionBackgroundColor,
!platformDrawsTextControls
)
}
}
Expand Down Expand Up @@ -456,7 +458,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 @@ -1108,6 +1110,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)
this@TextFieldCoreModifierNode.drawPlatformSelection(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.
this@TextFieldCoreModifierNode.drawPlatformCursor(
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
@@ -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
@@ -0,0 +1,35 @@
/*
* 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.selection

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.unit.DpSize

@Composable
internal actual fun SelectionHandle(
offsetProvider: OffsetProvider,
isStartHandle: Boolean,
direction: ResolvedTextDirection,
handlesCrossed: Boolean,
minTouchTargetSize: DpSize,
lineHeight: Float,
modifier: Modifier
) {
// TODO: check that JVM target shouldn't require selection handles
}
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)
Loading
Loading