Почему мы построили приложение на едином ядре Rust с отдельными оболочками для каждой платформы
Чтобы выпустить один и тот же редактор для трёх ОС, можно создать интерфейс всего один раз. Мы однажды пошли этим путём, затем вернулись и разместили поверх единого ядра набор тонких оболочек для каждой платформы.

Чтобы выпустить один и тот же редактор для Linux, macOS и Windows, интерфейс приходится создавать трижды.
Сначала мы хотели избежать этих затрат. С помощью одного кроссплатформенного инструментария мы отрисовали одинаковый интерфейс во всех трёх операционных системах. Благодаря этому решению мы преодолели первый рубеж производительности и достигли первого этапа поддержки корейского ввода.
Но затем приоритеты изменились.
Производительность важнее дизайна, а дизайн важнее стоимости. Время и стоимость исключаются из числа ограничений.
Когда мы снова посмотрели на задачу с этими критериями, ответ тоже изменился.
Мы отказались от единого интерфейса
У кроссплатформенного инструментария есть очевидное преимущество. Созданную один раз функцию можно использовать в нескольких операционных системах.
Но за это приходилось платить. Он потреблял на 40–60 МБ больше памяти, а запуск занимал на 150–200 мс дольше. Отображение символов уступало штатным текстовым движкам операционных систем, а использовать системные визуальные эффекты в полной мере было невозможно.
Когда стоимость разработки имела значение, это был разумный выбор. Но после того как мы исключили стоимость из числа ограничений, мириться с этим больше не было причин.
Поэтому мы решили создать ядро для обработки документов на Rust, а отвечающие за интерфейс оболочки разрабатывать отдельно для каждой операционной системы.
Дело не в том, что прежняя технология была плохой. Когда меняются приоритеты, среди тех же вариантов находится другой ответ. Мы решили заплатить за тройную реализацию функций, чтобы вернуть производительность, качество текста и ощущение, свойственное каждой системе.
Однако мы отказались не от всего.
Набор правил измерения мы сохранили без изменений. Держать указатель за пределами окна, безошибочно находить окно по имени, синхронно подсчитывать кадры. Вместе с ними мы перенесли и принципы проектирования корейского ввода. Операционной системе мы показываем только строку с курсором и не оформляем символы, находящиеся в процессе композиции. Если состояния расходятся, мы ничего не предполагаем, а заново их синхронизируем.
Код можно выбросить, но проверенные принципы остаются.
Мы свели роль оболочки к минимуму
Если платформенные оболочки начинают брать на себя слишком много задач, три продукта быстро расходятся. Поэтому мы чётко определили, что оболочка должна и чего не должна делать.
Оболочка создаёт окно и отрисовывает текст. Она передаёт ядру ввод с клавиатуры и указателя, а корейский ввод преобразует в команды редактирования. Она также применяет визуальные эффекты операционной системы.
При этом оболочка не разбирает Markdown. За исключением переноса строк, она не определяет компоновку, не сохраняет файлы и не принимает решений о лицензии.
Этот принцип существует не только в документации. Если в оболочке появляется код для разбора Markdown, автоматическая проверка останавливает сборку.
Если синтаксис начинает обрабатываться только в одной оболочке, результаты в разных операционных системах могут понемногу разойтись. Такие проблемы ещё и трудно обнаруживать. Если таблица выглядит иначе лишь в одной оболочке, настоящая причина может быть вовсе не в коде, который её отрисовывает.
Мы сделали так, чтобы надолго оставалось только ядро
Ядро не зависит ни от одной технологии пользовательского интерфейса. Его можно тестировать отдельно в терминале, даже без окна.
Ядро отвечает за редактирование документов и разбор Markdown, отмену действий, поиск, структуру документа, сохранение файлов, экспорт и проверку лицензии.

Когда пользователь нажимает клавишу, оболочка передаёт этот ввод ядру. Ядро изменяет документ и повторно разбирает только область вокруг изменённого фрагмента. Затем оно вычисляет лишь видимое на экране содержимое и возвращает оболочке изменившийся результат. Оболочка перерисовывает только затронутые строки и завершает один кадр.
Если в ходе этого процесса документ каждый раз перечитывается целиком, это не просто проблема производительности. Это нарушение изначально установленного бюджета.
Корректность тоже проверяется один раз — в ядре. Оно должно пройти все 652 официальных примера CommonMark, а если число успешно пройденных примеров уменьшается, сборка завершается с ошибкой. Мы также проводим фаззинг-тестирование, непрерывно подавая непредвиденные входные данные.
Мы проверяем и то, совпадает ли сохранённый файл с оригиналом побайтно. Если коэффициент обратного преобразования меньше 1,0, это означает не медленную работу, а потерю файла.
Даже если оболочка меняется, ядро остаётся прежним. В Linux и Windows ядро подключается напрямую, а в macOS используется связующий код на Swift. Оболочка для macOS создаётся на AppKit и TextKit 2, а оболочка для Windows — на DirectWrite.
Наша цель — сохранить документы одинаковыми, но сделать ощущения от работы естественными для каждой операционной системы.
Документы одинаковы, а ощущения от работы различаются
После того как мы определили границы, стало ясно и то, что именно необходимо унифицировать.
Унифицировать нужно документы. В какой бы операционной системе ни был сохранён файл, его содержимое должно совпадать побайтно. Не должны различаться ни выравнивание таблиц, ни окончания строк, ни завершающий перевод строки.
Корректность Markdown уже проверяется в ядре. Доказывать её заново при создании каждой новой оболочки не нужно. На новой платформе необходимо проверить ощущения от работы и производительность.
А вот непосредственное взаимодействие мы, напротив, не унифицируем.
Текст отрисовывается текстовым движком каждой операционной системы. Для корейского ввода также напрямую используется механизм операционной системы. Эффекты полупрозрачности в macOS и Windows реализуются системными средствами, а в Linux, где такой возможности нет, мы самостоятельно отрисовываем только необходимые части.
Мы не пытаемся насильно унифицировать положение кнопок окна, вид меню и клавиши-модификаторы в сочетаниях клавиш. Наша цель не в том, чтобы показывать одинаковые пиксели в разных операционных системах. Гораздо важнее, чтобы в каждой системе всё ощущалось естественно.
Условия выпуска мы тоже установили отдельно для каждой операционной системы. Выпуск возможен только после прохождения требований по производительности и практического тестирования со стандартным средством корейского ввода.
Строка соответствующей платформы появляется в таблице результатов тестирования лишь после того, как для неё действительно готова сборка. Если заранее вписать строку, которую ещё не измеряли, доверять остальным строкам таблицы тоже станет трудно.
У этого выбора есть очевидная цена
Одни и те же функции приходится создавать трижды. Поддержку корейского ввода тоже нужно реализовать трижды, как и поддерживать три комплекта окружений сборки.
На машине, которой мы сейчас пользуемся, невозможно собирать или отлаживать версии для Windows и macOS. Для этого нужны отдельное оборудование и среда автоматизации. Поэтому для этих двух платформ мы не указываем даты.
Мы также зафиксировали условия, при которых пересмотрим это решение. Его предпосылка состоит в том, что стоимость исключена из числа ограничений. Если получить необходимое оборудование не удастся, мы отложим только сроки соответствующей платформы. Выпуск версии для Linux продолжится по плану.
Для решения нужны не только причины, но и условия, при которых его следует пересмотреть. Тогда при изменении обстоятельств не придётся начинать спор с самого начала.
Надолго должно остаться ядро. Оболочку всегда должно быть можно выбросить.
Платформам мы доверили только то, что можно выбросить.