21 июля 2008 г.

История одного проекта 2

Итак, ранее я познакомил вас со своей дурной идеей, пришло время перейти к тому, как все это было реализовано.
Как я уже говорил, раз уж я решил ограничится моноширинными шрифтами, то вся рабочая область редактора может быть представлена прямоугольным двумерным массивом символов. Такой взгляд на текст чрезвычайно удобен с точки зрения прорисовки. Действительно, определили, какая часть этого прямоугольника видна на экране - и перерисовываем подряд все клетки. С другой стороны, мы имеем дело с текстом, а при работе с текстом крайне удобно иметь, кроме адресации "строка-столбец", еще и последовательную адресацию. К тому же, хранить текст в прямоугольном массиве крайне нерационально.
Поскольку все мои мысли были заняты именно графикой, то я решил хранить текст в виде списка строк. На языке Си это можно представить так:

struct ApxString {
    char *chars; // строка символов
    short *styles; // строка стилей символов
    size_t size; // выделенная длина строк
    size_t length; // длина текста в строке
} buffer[]; // сам буфер в виде масива строк
size_t bufsize; // размер буфера
Возможно, именно в этом заключалась моя главная ошибка. Ведь для работы с этой структурой совершенно невозможно было использовать никакие библиотечные функции, хотя бы потому, что необходимо было параллельно каждой манипуляции со строкой символов делать то же со строкой стилей (которые еще short по давно забытой причине, хотя char'а бы вполне хватило).
Первым делом я решил абстрагироваться от тонкостей управления памятью. Это было достигнуто введением свойств CharAt[Row, Col], StyleAt[Row, Col] и RowLengths[Row, Col]>, которые, соответственно, позволяют прочитать или установить символ, стиль или использованную длину строки. Причем эти свойства ведут себя корректно при любых значениях параметров, автоматически перераспределяя память, если необходимо, и игнорируя отрицательные значения. Если символ не определен, то возвращается #0, если стиль не определен - возвращается стиль конца строки. Полная иллюзия бесконечного прямоугольного массива символов и стилей. Конечно, во всякой нетривиальной абстракции есть дыры. В данном случае дыра - это ограниченность памяти. Но я решил пренебречь этим ограничением, поскольку мегабайт текста в этой структуре будет занимать порядка 10MB: по 3 байта на символ, плюс 8 байт на длины, плюс запас на округление длины каждой строки вверх, скажем, до 20 символов. Так что все действительно не так уж страшно.
Осталось построить интерфейс для доступа тексту как к одной строке. Для этого я просто написал функции преобразования индексов - из двумерных в одномерный и обратно. Теперь для доступа к n-му символу нужно написать что-то вроде CharAt[IndexToPos(n)].

Выглядит неплохо, не так ли? Нет! Здесь допущена принципиальная ошибка: интерфейс удобен для самого компонента, но не для тех кто им пользуется. Никому не интересно внутреннее представление текста. Им нужен сам текст. И возможность работы с этим текстом как с обычной строкой. Поэтому более правильным, как мне сейчас кажется, было бы хранение текста в виде непрерывного массива с линейной адресацией, с каким-нибудь умным механизмом отображения символов на прямоугольную решетку.
В широко известном компоненте Scintilla применяется именно список строк, видимо, по соображениям производительности.

Поверх рассмотренной абстракции лежит слой функций для манипуляции текстом - вставка и удаление произвольных кусков текста и целых строк. Также этот слой ответственен за поддержку отмены, что само по себе заслуживает упоминания, поскольку я совершенно забыл об этой функции, пока не сделал первую рабочую версию редактора. Внедрение какого-то подобия отмены стало настоящим кошмаром. Признаюсь, она и сейчас работает весьма криво. Архитектурные недоработки даром не проходят!

Теперь, когда появились высокоуровневые средства манипуляции текстом, можно строить слой обработки действий пользователя. Фактически вся работа делается в обработчиках KeyDown и KeyPress, первый из которых вызывается для всех клавиш, а второй - для символьных и некоторых командных (например, я очень удивился, когда узнал, что Ctrl+A - на самом деле символ с кодом 1).
Вот тут писать пришлось много. Вот лишь неполный список того, на что должно реагировать уважающее себя текстовое поле:

  • символьные клавиши
  • Enter
  • Delete и Backspace
  • PageUp/PageDown
  • Стрелки
  • Ctrl+Delete и Ctrl+Backspace
  • Ctrl+Стрелки
Некоторые из этих команд настолько естественны, что их наличие даже не осознаешь. Например, Ctrl+Backspace. Естественно, когда я начал пользоваться собственным редактором, тут же выявил кучу недоделок. Eat your own dog food!.

А как же градиентная прорисовка, из-за которой, собственно, все и затевалось? Об этом чуть позже.

Комментариев нет: