Элементы управления TextInput являются сложными
Позвольте мне начать с того, что я не являюсь экспертом в области управления текстами, но сейчас я уверен, что это не имеет значения, потому что я могу помочь вам безопасно добраться до леса. Эти вещи сложны по своей природе и требуют большой интуиции и знаний о том, как все работает. Однако вы можете проверить код, который выполняется в senpai-js/senpai-stage
хранилище здесь .
Мы должны определить несколько вещей заранее:
- Текст может быть любым допустимым символом Юникода. Вы можете разобрать это, используя это регулярное выражение:
/^.$/u
- Вам необходимо отслеживать три различных режима редактирования текста:
Insert
, Selection
, Basic
(я использую перечисление SelectionState
в своей библиотеке и проверяю свойство insertMode
на сцене)
- Вы должны выполнять проверки работоспособности на каждом шагу, иначе у вас будет неопределенное и неожиданное поведение
- Большинство людей ожидает, что ввод текста будет иметь большую ширину, поэтому убедитесь, что вы используете шаблон для внутренней части текстового поля, если вы планируете использовать текстуру
- Обнаружение столкновения мыши / точки касания сложно, если только вы не гарантируете, что элемент управления вводом текста не будет вращаться
- Текст должен прокручиваться, когда он больше, чем текстовое поле в горизонтальном направлении. Мы будем называть это
textScroll
, которое всегда является отрицательным числом
Теперь я перейду к каждой функции, чтобы описать ее поведение, чтобы точно описать, как должен работать элемент управления textbox.
Столкновение (широкая фаза и узкая фаза)
Обнаружение столкновения - это монстр. Нормализация движения точек между событиями мыши и касания - сложный зверь, не описанный в этом тексте. Как только вы обработаете точечные события, вы должны выполнить какое-то общее обнаружение столкновения для прямоугольника. Это значит делать столкновение AABB. Если сам текстовый спрайт вращается, вам придется «разворачивать» саму точку. Однако мы пропускаем эту проверку, если точка мыши / касания уже находится над текстовым полем. Это потому, что как только вы начинаете выделять текст, вы хотите, чтобы эта функция всегда возвращала true
. Затем мы переходим к узкому столкновению фаз, которое фактически проверяет, находится ли «не преобразованная» точка мыши / касания в пределах отступа текстового поля. Если это так или текстовое поле активно, мы возвращаем истинное значение здесь.
Как только мы узнаем, что точка мыши / касания находится в границах нашего текстового поля, мы визуально изменим CSS холста на cursor: text;
.
pointCollision
Когда мы нажимаем кнопку мыши над текстовым полем, нам нужно вычислить, куда перемещать каретку. Каретка может существовать в диапазоне от 0
до text.length
включительно. Обратите внимание, что это не совсем правильно, потому что символы Юникода могут иметь длину 2
. Вы должны отслеживать каждый символ, добавленный к вашему тексту внутри массива, чтобы утверждать, что вы не измеряете неисправные символы Юникода. Вычисление целевого индекса означает зацикливание на каждом символе текущего текста и добавление его во временную строку, каждый раз измеряя, пока измеренная ширина не станет больше текущей textScroll + измеренная textWidth.
Как только мы убедимся, что точка опустилась поверх текстового поля и начальная точка установлена, мы можем запустить режим «выбора». Перетаскивание точки должно переместить выделение из начального caretIndex в новый вычисленный конечный индекс. Это идет в обоих направлениях.
Пример этого показан здесь .
нажатия
Решением для нажатий веб-клавиш является проверка свойства key
в KeyEvent. Несмотря на многое из того, что говорят все, можно проверить это свойство текста, сравнив его с вышеупомянутым регулярным выражением Юникода. Если он совпадает, скорее всего, клавиша фактически была нажата на клавиатуре. Это не учитывает комбинации клавиш, такие как ctrl + c
и ctrl + v
для копирования и вставки. Эти функции тривиальны и читатель сам решает, как их реализовать.
Исключением являются клавиши со стрелками: «ArrowLeft», «ArrowRight» и т. Д. Эти клавиши фактически изменяют состояние вашего элемента управления и изменяют его функции. Важно помнить, что ключевые события должны обрабатываться только текущим focused
элементом управления. Это означает, что вы должны проверить и убедиться, что элемент управления сфокусирован во время ввода текста. Это, конечно, происходит на более высоком уровне, чем я кодировал в своей библиотеке, так что это тривиально.
Следующая проблема, которая должна быть решена, заключается в том, как каждый вводимый символ должен изменять состояние вашего элемента управления. Метод keyDown
распознает selectionState
и вызывает другую функцию в зависимости от его состояния. Это не оптимизированный псевдокод, но используется для ясности и идеально подходит для наших целей при описании поведения.
нажатие клавиши выбора
- Обычные нажатия клавиш заменяют содержимое выделенного текста
- Сращивание с
selectionStart
и вставка нового ключа в текстовый массив
- Если нажата кнопка «Удалить» или «Возврат», объединить выбор и вернуть режим выбора на
Normal
или Caret
- если нажата клавиша «влево» или «вправо», переместите курсор в начало или конец соответственно и верните режим выбора в «Нормальный», если не нажата клавиша Shift
- если нажата клавиша Shift, то мы действительно хотим расширить выбор
- начало выбора всегда будет в caretIndex, и мы, по сути, перемещаем конечную точку выбора влево или вправо с помощью этой комбинации клавиш
- если конец выбора возвращается к индексу каретки, мы возвращаем
selectionState
к Normal
снова
- клавиши «home» и «end» работают одинаково, только указатель перемещается в индексы
0
и text.length
соответственно
- также обратите внимание, что нажатие клавиши Shift расширяет выбор с
caretIndex
еще раз
нажатие клавиши в обычном режиме (режим каретки)
- в режиме каретки, мы не заменяем текст, просто вставляем новые символы в текущей позиции
- нажатия клавиш, которые соответствуют регулярному выражению Юникода, вставляются с использованием метода сращивания
- переместите каретку вправо после сращивания текста (проверьте и убедитесь, что вы не перебираете длину текста)
- Backspace удаляет один символ перед индексом в
caretIndex - 1
- Удалить удаляет один символ после индекса в
caretIndex
- выбор текста применяется для левой и правой клавиш при нажатой клавише Shift
- когда смена не нажата, влево и вправо перемещают каретку влево и вправо соответственно
- ключ home устанавливает значение caretIndex равным
0
- ключ конца устанавливает значение caretIndex равным
text.length
keyDown в режиме вставки
- в режиме вставки мы заменяем текущий выбранный символ на
caretIndex
- нажатия клавиш, соответствующие регулярному выражению Юникода, вставляются с использованием метода сращивания
- переместите каретку вправо после сращивания текста (проверьте и убедитесь, что вы не перебираете длину текста)
- Забой удалить символ ДО текущего выбора
- delete удаляет текущий выбранный символ
- клавиши со стрелками работают как положено и описаны в обычном режиме
- клавиши home и end работают как положено и описаны в обычном режиме
обновление текстового поля каждый кадр
- Если текстовое поле сфокусировано, вы должны начать мигать кареткой, чтобы пользователь знал, что он редактирует текст в текстовом поле
- при перемещении каретки влево или вправо в режиме
Caret
необходимо перезапустить механизм вспышки, чтобы он точно показывал, где они находятся при каждом перемещении каретки
- Вспышка каретки примерно каждые 30 кадров или полсекунды
- измерьте расстояние каретки по тексту, используя
ctx.measureText
до индекса каретки, разделив текст на позицию каретки, если режим не равен Selection
- По-прежнему полезно измерять, как далеко вдоль текста находится в режиме выделения
Selection
, потому что мы всегда хотим, чтобы конец выделения текста был видимым для пользователя
- Убедитесь, что каретка всегда видна в пределах видимых границ текстового поля, с учетом текущего textScroll
рендеринг текстового поля
- сначала сохранить контекст
ctx.save()
(базовый холст)
- если вы не рисуете текстовое поле с контурами, нарисуйте левую кепку текстового поля, нарисуйте средний рисунок и правую кепку соответственно на первом слое
- использовать путь, заданный отступом и размером текстового поля, чтобы вырезать квадрат, чтобы предотвратить вытекание текста
- преобразовать в значение x
textScroll
, которое должно быть отрицательным числом
- перевести на значение y
midline
, которое должно быть по центру текстового поля по вертикали
- установить свойство шрифта
- установите базовый текст для
middle
и заполните текст, вызвав text.join("")
в вашем текстовом массиве
- если есть выделение или режим вставки, обязательно нарисуйте «синий» квадрат позади выделенного текста и инвертируйте цвет шрифта выделенного текста (это нетривиально и оставлено читателю в качестве упражнения)
Заключительные замечания
Да, задача чудовищная, но иногда вы просто хотите дать миру визуальный новый движок или сцену со спрайтами со всевозможными функциями. Пожалуйста, посетите репозиторий senpai-stage
и отправьте запрос на извлечение, если вы чувствуете, что хотите, чтобы сэмпай заметил вас. Удачи в ваших начинаниях!
~ дьявол сэмпай