| /* |
| * Copyright 2019 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.ui.core |
| |
| import androidx.compose.composer |
| import androidx.compose.Composable |
| import androidx.compose.ambient |
| import androidx.compose.memo |
| import androidx.compose.state |
| import androidx.compose.unaryPlus |
| import androidx.ui.core.gesture.TouchSlopDragGestureDetector |
| import androidx.ui.core.gesture.DragObserver |
| import androidx.ui.core.gesture.PressGestureDetector |
| import androidx.ui.core.input.FocusManager |
| import androidx.ui.graphics.Color |
| import androidx.ui.input.EditProcessor |
| import androidx.ui.input.EditorModel |
| import androidx.ui.input.ImeAction |
| import androidx.ui.input.KeyboardType |
| import androidx.ui.text.TextDelegate |
| import androidx.ui.text.TextStyle |
| |
| /** |
| * Data class holding text display attributes used for editors. |
| */ |
| data class EditorStyle( |
| /** The editor text style */ |
| val textStyle: TextStyle? = null, |
| |
| /** |
| * The composition background color |
| * |
| * @see EditorModel.composition |
| */ |
| val compositionColor: Color = Color(alpha = 0xFF, red = 0xB0, green = 0xE0, blue = 0xE6), |
| |
| /** |
| * The selection background color |
| * |
| * @see EditorModel.selection |
| */ |
| // TODO(nona): share with Text.DEFAULT_SELECTION_COLOR |
| val selectionColor: Color = Color(alpha = 0x66, red = 0x33, green = 0xB5, blue = 0xE5) |
| ) |
| |
| /** |
| * A default implementation of TextField |
| * |
| * To make TextField work with platoform input service, you must keep the editor state and update |
| * in [onValueChagne] callback. |
| * |
| * Example: |
| * var state = +state { EditorModel() } |
| * TextField( |
| * value = state.value, |
| * onValueChange = { state.value = it }) |
| */ |
| @Composable |
| fun TextField( |
| /** Initial editor state value */ |
| value: EditorModel, |
| |
| /** The editor style */ |
| editorStyle: EditorStyle, |
| |
| /** |
| * The keyboard type to be used in this text field. |
| * |
| * Note that this input type is honored by IME and shows corresponding keyboard but this is not |
| * guaranteed. For example, some IME may send non-ASCII character even if you set |
| * [KeyboardType.Ascii] |
| */ |
| keyboardType: KeyboardType = KeyboardType.Text, |
| |
| /** |
| * The IME action |
| * |
| * This IME action is honored by IME and may show specific icons on the keyboard. For example, |
| * search icon may be shown if [ImeAction.Search] is specified. Then, when user tap that key, |
| * the [onImeActionPerformed] callback is called with specified ImeAction. |
| */ |
| imeAction: ImeAction = ImeAction.Unspecified, |
| |
| /** Called when the InputMethodService update the editor state */ |
| onValueChange: (EditorModel) -> Unit = {}, |
| |
| /** Called when the input field gains focus. */ |
| onFocus: () -> Unit = {}, |
| |
| /** Called when the input field loses focus. */ |
| onBlur: () -> Unit = {}, |
| |
| /** Called when the InputMethod requested an IME action */ |
| onImeActionPerformed: (ImeAction) -> Unit = {}, |
| |
| /** |
| * Optional visual filter for changing visual output of input field. |
| */ |
| visualTransformation: VisualTransformation? = null |
| ) { |
| // Ambients |
| val style = +ambient(CurrentTextStyleAmbient) |
| val textInputService = +ambient(TextInputServiceAmbient) |
| val density = +ambient(DensityAmbient) |
| val resourceLoader = +ambient(FontLoaderAmbient) |
| |
| // Memos |
| val processor = +memo { EditProcessor() } |
| val mergedStyle = style.merge(editorStyle.textStyle) |
| val (visualText, offsetMap) = +memo(value, visualTransformation) { |
| TextFieldDelegate.applyVisualFilter(value, visualTransformation) |
| } |
| val textPainter = +memo(visualText, mergedStyle, density, resourceLoader) { |
| // TODO(nona): Add parameter for text direction, softwrap, etc. |
| TextDelegate( |
| text = visualText, |
| style = mergedStyle, |
| density = density, |
| resourceLoader = resourceLoader |
| ) |
| } |
| |
| // States |
| val hasFocus = +state { false } |
| val coords = +state<LayoutCoordinates?> { null } |
| |
| processor.onNewState(value, textInputService) |
| TextInputEventObserver( |
| onPress = { }, |
| onFocus = { |
| hasFocus.value = true |
| TextFieldDelegate.onFocus( |
| textInputService, |
| value, |
| processor, |
| keyboardType, |
| imeAction, |
| onValueChange, |
| onImeActionPerformed) |
| coords.value?.let { coords -> |
| textInputService?.let { textInputService -> |
| TextFieldDelegate.notifyFocusedRect( |
| value, |
| textPainter, |
| coords, |
| textInputService, |
| hasFocus.value, |
| offsetMap |
| ) |
| } |
| } |
| onFocus() |
| }, |
| onBlur = { |
| hasFocus.value = false |
| TextFieldDelegate.onBlur( |
| textInputService, |
| processor, |
| onValueChange) |
| onBlur() |
| }, |
| onDragAt = { TextFieldDelegate.onDragAt(it) }, |
| onRelease = { |
| TextFieldDelegate.onRelease( |
| it, |
| textPainter, |
| processor, |
| offsetMap, |
| onValueChange, |
| textInputService, |
| hasFocus.value) |
| } |
| ) { |
| Layout( |
| children = @Composable { |
| OnPositioned { |
| if (textInputService != null) { |
| // TODO(nona): notify focused rect in onPreDraw equivalent callback for |
| // supporting multiline text. |
| coords.value = it |
| TextFieldDelegate.notifyFocusedRect( |
| value, |
| textPainter, |
| it, |
| textInputService, |
| hasFocus.value, |
| offsetMap |
| ) |
| } |
| } |
| Draw { canvas, _ -> TextFieldDelegate.draw( |
| canvas, |
| value, |
| offsetMap, |
| textPainter, |
| hasFocus.value, |
| editorStyle) } |
| }, |
| measureBlock = { _, constraints -> |
| TextFieldDelegate.layout(textPainter, constraints).let { |
| layout(it.first, it.second) {} |
| } |
| } |
| ) |
| } |
| } |
| |
| /** |
| * Helper composable for observing all text input related events. |
| */ |
| @Composable |
| private fun TextInputEventObserver( |
| onPress: (PxPosition) -> Unit, |
| onDragAt: (PxPosition) -> Unit, |
| onRelease: (PxPosition) -> Unit, |
| onFocus: () -> Unit, |
| onBlur: () -> Unit, |
| children: @Composable() () -> Unit |
| ) { |
| val focused = +state { false } |
| val focusManager = +ambient(FocusManagerAmbient) |
| |
| DragPositionGestureDetector( |
| onPress = { |
| if (focused.value) { |
| onPress(it) |
| } else { |
| focusManager.requestFocus(object : FocusManager.FocusNode { |
| override fun onFocus() { |
| onFocus() |
| focused.value = true |
| } |
| |
| override fun onBlur() { |
| onBlur() |
| focused.value = false |
| } |
| }) |
| } |
| }, |
| onDragAt = onDragAt, |
| onRelease = onRelease |
| ) { |
| children() |
| } |
| } |
| |
| /** |
| * Helper class for tracking dragging event. |
| */ |
| internal class DragEventTracker { |
| private var origin = PxPosition.Origin |
| private var distance = PxPosition.Origin |
| |
| /** |
| * Restart the tracking from given origin. |
| * |
| * @param origin The origin of the drag gesture. |
| */ |
| fun init(origin: PxPosition) { |
| this.origin = origin |
| } |
| |
| /** |
| * Pass distance parameter called by DragGestureDetector$onDrag callback |
| * |
| * @param distance The distance from the origin of the drag origin. |
| */ |
| fun onDrag(distance: PxPosition) { |
| this.distance = distance |
| } |
| |
| /** |
| * Returns the current position. |
| * |
| * @return The position of the current drag point. |
| */ |
| fun getPosition(): PxPosition { |
| return origin + distance |
| } |
| } |
| |
| /** |
| * Helper composable for tracking drag position. |
| */ |
| @Composable |
| private fun DragPositionGestureDetector( |
| onPress: (PxPosition) -> Unit, |
| onDragAt: (PxPosition) -> Unit, |
| onRelease: (PxPosition) -> Unit, |
| children: @Composable() () -> Unit |
| ) { |
| val tracker = +state { DragEventTracker() } |
| PressGestureDetector( |
| onPress = { |
| tracker.value.init(it) |
| onPress(it) |
| }, |
| onRelease = { onRelease(tracker.value.getPosition()) } |
| ) { |
| TouchSlopDragGestureDetector( |
| dragObserver = object : DragObserver { |
| override fun onDrag(dragDistance: PxPosition): PxPosition { |
| tracker.value.onDrag(dragDistance) |
| onDragAt(tracker.value.getPosition()) |
| return tracker.value.getPosition() |
| } |
| } |
| ) { |
| children() |
| } |
| } |
| } |