ВозможностиЦеныДокументацияБлог
Получить mote
← Блог
Дизайн·28 июля 2026 г.·4 мин чтения

Как мы сделали так, чтобы текст не сдвигался при появлении символов

Бывало ли у вас, что стоило лишь переместить курсор к элементу списка, как весь текст один раз сдвигался в сторону? С этим сталкиваются почти во всех редакторах, которые скрывают и показывают служебные символы.

m
mote
Два состояния одного и того же списка. Сверху каретка находится в тексте, поэтому отображается маркер списка, а снизу каретка находится на символе, поэтому виден исходный дефис. Левый край основного текста в обоих случаях остаётся на одном месте.
Два состояния одного и того же списка. Сверху каретка находится в тексте, поэтому отображается маркер списка, а снизу каретка находится на символе, поэтому виден исходный дефис. Левый край основного текста в обоих случаях остаётся на одном месте.

Когда я перемещал каретку, чтобы отредактировать список, текст сдвигался в сторону. Когда каретка уходила, текст возвращался обратно.

Двигалась всего одна строка, но казалось, будто дрожит весь абзац. Эта небольшая разница постоянно нарушала ход редактирования.

Причиной была ширина маркера.

Обычно на экране отображается круглый маркер списка. Но в файле хранятся дефис и пробел. Когда каретка попадает на маркер, нужно показать исходный текст, чтобы его можно было отредактировать напрямую.

Проблема заключалась в том, что эти два варианта занимали разную ширину. Маркер списка находится в блоке фиксированной ширины, а ширина дефиса и пробела зависит от шрифта. В mote эта разница составляла 4,9 px. Каждый раз, когда появлялся исходный текст, расположенный после него текст сдвигался на это расстояние.

Сдвигаем строку на разницу в ширине

Мы не стали делать ширину одинаковой. Вместо этого мы компенсируем разницу.

В тот момент, когда маркер заменяется исходным текстом, вся строка сдвигается в противоположную сторону на разницу в ширине. Форма символа меняется, но основной текст начинается на прежнем месте.

Файл при этом не изменяется. Маркер списка — это декоративный элемент, нарисованный поверх дефиса, а компенсация меняет только положение строки на экране. Когда каретка входит в маркер и выходит из него, сохранённое содержимое остаётся неизменным.

Мы также свели к одному источнику способ расчёта ширины. Раньше в месте отрисовки маркера и в месте коррекции строки использовались отдельные числовые значения. Небольшая разница между ними и составляла те самые 4,9 px.

Теперь в обоих местах ширина маркера запрашивается у одной и той же функции. Поскольку источник один, расхождений больше не возникает.

Однако если слева недостаточно свободного места, коррекция не применяется. Иначе после сдвига строки текст может оказаться обрезанным за границей окна. Исчезновение текста — более серьёзная проблема, чем дрожание.

Заголовки и цитаты мы оставили сдвигающимися

Сначала мы применили тот же подход к заголовкам и цитатам.

Но # в заголовке и > в цитате обычно занимают на экране нулевую ширину. Чтобы зафиксировать основной текст, пришлось бы вынести символы в левое поле.

Технически всё работало хорошо. Текст не двигался. Но экран выглядел непривычно. Символы, отдельно висящие в поле, смотрелись неестественно.

Поэтому мы отменили это изменение. Теперь, когда появляются символы заголовков и цитат, основной текст сдвигается вправо.

Экран, на котором исходный символ # приглушённо отображается только в строке заголовка с кареткой — занимая место, он сдвинул текст заголовка вправо, а у заголовка ниже символ не виден
Экран, на котором исходный символ # приглушённо отображается только в строке заголовка с кареткой — занимая место, он сдвинул текст заголовка вправо, а у заголовка ниже символ не виден

В итоге для каждого типа маркера мы выбрали своё решение.

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

Естественный вид экрана оказался важнее единого правила.

Показываем символы только тогда, когда они действительно нужны

Лучший способ уменьшить дрожание — сократить количество моментов, когда меняется сам символ.

Теперь символ списка отображается в исходном виде только тогда, когда каретка находится непосредственно на нём. Как только каретка проходит пробел и перемещается в основной текст, символ сразу снова становится маркером списка. Во время редактирования содержимого списка нет необходимости постоянно видеть дефис.

Выравнивание сохраняется, даже если длинная строка переносится на следующую. Пока маркер отображается в исходном виде, вторая строка всё равно начинается под основным текстом первой строки.

Мы также исправили проблему, из-за которой щелчок ошибочно распознавался как перетаскивание. Когда указатель немного дрожал в момент смены маркера, текст иногда выделялся. Теперь движение считается перетаскиванием, только если указатель переместился более чем на 8 px.

Проверяем числами, а не глазами

Такие различия трудно проверять только глазами. При беглом взгляде всё может казаться нормальным, но при длительной работе экран воспринимается как беспокойный.

Поэтому мы создали тест, который запускает настоящее приложение. Он перемещает каретку внутрь маркера и за его пределы, а затем напрямую сравнивает координаты x, с которых начинается основной текст. Тест проходит только в том случае, если оба значения совпадают.

Тем же способом проверяются маркеры списка, флажки задач и вложенные списки.

Смещение на один пиксель редко оформляют как отчёт об ошибке. Пользователю проще почувствовать, что «всё почему-то мельтешит», чем объяснить, что именно сдвинулось.

Поэтому за такими движениями должны следить не люди, а числа.

Спокойный экран — не тот, на котором ничего не происходит. Это экран, на котором необходимые изменения происходят там, где они не раздражают глаз.

← РанееПочему мы каждый раз тестируем приложение в восьми средах LinuxПозже →Почему символы Markdown показываются только в слове под курсором
Следите за новостями через RSS.
Оставьте только текст
Русский
© 2026 mote