MOffice

Своя оболочка MOffice Write (без Qt)

2026-10-04. Поручение владельца: «сделать такую же оболочку и использовать свою оболочку» (решение № 39). Снимки настоящего окна своей оболочки ui/meridian-write на собственном наборе компонентов ui/meridian-ui — рядом со снимками прототипа на Qt 6.8 («Живая оболочка»). Страницы документов рисует ядро через тот же C ABI meridian-ffi, примеры — реальные docx GovDocs1 (общественное достояние). Анализ вариантов, оценки и план — docs/research/own-shell.md.

Как сделано

Окно, ввод и вывод кадра — свой слой meridian-platform (Cocoa, Win32, X11 — без сторонних библиотек), растр — свой meridian-raster (общий с ядром), диалог «Открыть» — свой; текст — свой meridian-font (общий с ядром, сессия A: разбор OpenType, метрики, контуры, кернинг из GPOS); шрифт интерфейса Inter 4.1 вшит. Всё остальное — свой код: компоненты MDS (копии shell/mds/components/*.qml), ввод, иконки, лента с адаптивным сворачиванием групп, холст, панели, палитра команд, backstage. Цвета и размеры — из spec/design-tokens.json через тот же генератор, что делает QML-токены. Все зависимости — классы 0–1 политики лицензий (cargo deny — ok), Qt не нужен ни при сборке, ни при запуске.

Сравнение с Qt-прототипом

Сходство кадров считается SSIM (1.0 — совпадение до пикселя) по окнам 8×8 после размытия 1 px — тем же способом, каким ядро сравнивает страницы с эталоном. Мелкие различия: образцы стилей у своей оболочки набраны Carlito, как требует токен fontFamily.document (у Qt на этой машине Carlito не установлен, и он берёт системный шрифт); знак «⌄» и «⇧» — из запасного шрифта.

Сцена SSIM
Светлая тема, 1440×900, реальный docx (7 страниц) 0.985
Тёмная тема, docx с табуляциями 0.983
Палитра команд 0.968
Backstage, тёмная тема 0.996
Узкое окно 1024×700 (адаптивная лента) 0.984
Новый документ 0.982

Светлая тема — своя оболочка

Своя оболочка, светлая тема

Светлая тема — Qt 6.8 (для сравнения)

Qt-прототип, светлая тема

Тёмная тема — своя оболочка

Своя оболочка, тёмная тема

Палитра команд ⌘⇧P / Ctrl+Shift+P — своя оболочка

Палитра команд

Список шрифтов на ленте — предпросмотр семейств

Список шрифтов

Панель «Навигация» — оглавление документа из ядра (FFI v0.3)

Заголовки с отступом по уровню (w:outlineLvl), текущий раздел подсвечен по прокрутке, щелчок — переход к странице заголовка; длинный список прокручивается колесом. Новый документ создаётся по локали: для ru-RU — A4 с полями Word.

Оглавление

Вкладка «Вид», тема «Высокий контраст»

Высокий контраст

Фокус с клавиатуры — Tab дошёл до вкладки «Вид»

Tab и Shift+Tab ходят по всем кнопкам, вкладкам и строкам списков в порядке кадра, стрелки — к ближайшему элементу в ту сторону (сначала в том же ряду: по вкладкам, вниз — в команды ленты), Enter и пробел нажимают элемент в фокусе. Рамка — по токенам borderWidth.focus и focusOffset (2 px, цвет акцента); видна только после работы с клавиатуры и прячется при щелчке мышью. Открытая палитра, всплывающее окно, backstage или диалог «Открыть» замыкают фокус внутри себя.

Фокус с клавиатуры

Подсказки клавиш (Alt) — буквы у команд вкладки «Главная»

Одиночный Alt (на Windows и F10) показывает буквы у «Файл» и вкладок ленты, буква открывает вкладку и показывает коды её команд, следующий код нажимает команду; Esc — шаг назад, щелчок мышью — закрыть. Коды подбираются по подписям без повторов; если команд больше, чем букв, коды на уровне двухбуквенные, как в Office. Набор на латинской раскладке переводится по положению клавиш (ЙЦУКЕН), поэтому подсказки работают при любой раскладке. На macOS подсказки не включаются: там так не принято, Option набирает символы.

Подсказки клавиш

Всплывающая подсказка — название и сочетание клавиш

После 600 мс наведения под элементом появляется плашка с его названием и, если есть, сочетанием клавиш. После щелчка подсказка молчит, пока указатель не уйдёт с элемента. Кнопки, на которых только глиф или иконка («Ж», «⁝≡»), подписаны по-человечески («Полужирный», «Маркеры»): эти же названия используют коды Alt, тесты и будущий мост доступности. Окно перерисовывает кадр по таймеру, без постоянного опроса.

Всплывающая подсказка

Backstage — своя оболочка

Backstage

Узкое окно 1024×700 — группы ленты сворачиваются, панели скрыты

Узкое окно

Новый документ — пустой лист из ядра

Новый документ

Свой платформенный слой: настоящее окно macOS (снято системой)

Окно, ввод и вывод кадра — свой ui/meridian-platform без сторонних библиотек (решение № 40): AppKit и CoreGraphics через objc runtime. Снимок сделан утилитой macOS screencapture -l только этого окна: системные кнопки поверх своего заголовка, тема и Retina — от системы.

Окно macOS

Меню macOS — своим кодом через Cocoa

Строка меню: «MOffice Write» (о программе, скрыть, выйти), «Файл» (новый ⌘N, открыть ⌘O, закрыть ⌘W), «Правка» (стандартные пункты ОС — заработают с правкой документа), «Вид» (палитра ⇧⌘P, лента, масштаб ⌘=/⌘-/⌘0, панели, темы), «Окно» (свернуть ⌘M, масштабировать). Свои пункты шлют оболочке Event::Menu(id); клавиши с ⌘ перехватывает само меню, как у всех программ macOS. Проверено на настоящем окне: MERIDIAN_DUMP_MENU=1 печатает меню, прочитанное обратно из объектов NSMenu (пять меню, сочетания ⌘/⇧/⌥); --menu 21 выполняет пункт «Тёмная тема» через performActionForItemAtIndex: — кадр тёмный, --menu 10 открывает палитру. Сочетания в палитре и подсказках на macOS записаны знаками ОС (⌘N, ⇧⌘P, ⌃F1).

Linux (X11) — настоящий ввод под Xvfb

Тот же слой на X11 (Xlib подгружается при запуске). На сервере под виртуальным экраном xdotool щёлкнул вкладку «Вид» и кнопку «Тёмная» — снимок окна средствами X (import -window):

X11: щелчки по ленте

Набор с клавиатуры: Ctrl+Shift+P, латиница «kontr», затем в русской раскладке «тёмн» (с «ё») — найдена «Тёмная тема», Enter включает её:

X11: набор текста

Linux (X11) — подсказки клавиш и изменение размера окна без рамки

Под Xvfb с оконным менеджером Openbox: xdotool key alt — буквы у вкладок, затем «b» (на ЙЦУКЕН — «И») открывает «Вид» с кодами команд; удержание Alt с автоповтором включает подсказки один раз (сверху вниз — три кадра).

Подсказки клавиш под X11

Окно без рамки оконного менеджера меняет размер за полосу у края (6 логических px, в углах 12): курсор меняется на стрелку, нажатие передаётся оконному менеджеру через _NET_WM_MOVERESIZE. Проверено xdotool: правый край — ширина 1440 → 1620 (до края экрана), правый нижний угол — −147 × −97 px; щелчок внутри окна после этого по-прежнему доходит до оболочки (вкладка «Ссылки»), раскладка перестроилась под 1473×803.

Окно после изменения размера под X11

На Windows и Linux кнопки окна рисует сама оболочка — справа в заголовке, по 46 px, как в Windows 11: «Свернуть», «Развернуть/Восстановить» (значок меняется в развёрнутом окне), «Закрыть» (при наведении красная); на macOS остаются системные кнопки. Проверено под Openbox щелчками xdotool: развернуть — 1800×1200 на весь экран, восстановить — 1440×900 на прежнем месте, свернуть — окно скрыто (IsUnMapped) и возвращается, закрыть — выход программы с кодом 0.

Свои кнопки окна под X11

Доступность — дерево для экранных чтецов (macOS)

Каждый кадр оболочка собирает дерево доступности из того, что уже знает: подписи элементов, роли (кнопка, вкладка, строка списка), рамки и фокус клавиатуры; порядок — смысловой (заголовок → лента → навигация → документ → стили → строка состояния), при открытой палитре или диалоге — только их элементы. На macOS дерево становится объектами NSAccessibilityElement под представлением окна: VoiceOver читает роли и подписи, нажатие экранным чтецом приходит оболочке щелчком. Проверено на настоящем окне: дерево прочитано обратно из объектов Cocoa (--ax-dump: 71 элемент, AXRadioButton «Главная», AXButton «Закрыть документ»…), --ax-press "Вид" через accessibilityPerformPress открывает вкладку «Вид». Windows (UI Automation) — следующий шаг.

Доступность на Linux — AT-SPI 2 через свой D-Bus

На Linux то же дерево отдаётся по AT-SPI 2 — протоколу, через который Orca и другие экранные чтецы читают программы. Клиент D-Bus свой (meridian-platform/src/dbus.rs, без libdbus): адрес unix:path=/unix:abstract=, вход EXTERNAL, сообщения little-endian со всеми типами D-Bus; кодирование сверено побайтно с эталонным Hello. Мост (atspi.rs) берёт адрес шины доступности у org.a11y.Bus (или из AT_SPI_BUS_ADDRESS), встраивается в реестр (Socket.Embed) и в своём потоке отвечает на вызовы: приложение → окно (frame) → кнопки, вкладки и строки списка с интерфейсами Accessible, Component (рамки в координатах экрана и окна, попадание по точке), Action (click), Application и Cache. DoAction приходит циклу окна событием AccessibilityPress — как нажатие VoiceOver на macOS. Смена состава, подписей и фокуса уходит сигналами object:children-changed, property-change:accessible-name, state-changed:focused, фокус окна — window:activate. NO_AT_BRIDGE=1 выключает мост, как в GTK.

Проверено на сервере под Xvfb с настоящими at-spi-bus-launcher и at-spi2-registryd и независимым клиентом pyatspi (библиотека libatspi, на которой работает Orca):

Редкие письменности — из системных шрифтов

Вшитых Inter, Carlito и Liberation хватает для латиницы, кириллицы и греческого; китайский, японский, корейский, армянский, грузинский, тайский и другие берутся из шрифтов ОС. Папки шрифтов просматриваются один раз, файлы читаются по мере надобности — сначала по подсказке письменности в имени, в коллекции выбирается обычное начертание; найденное и ненайденное кэшируется, объём прочитанного ограничен. На строку из семи письменностей macOS читается 5 файлов (Hiragino, Apple SD Gothic Neo, Noto Sans Armenian, SF Georgian, Thonburi). Снимок — сцена --scene scripts --system-fonts. Сложные письменности (арабская, деванагари) пока без шейпинга — его даст meridian-shape ядра.

Редкие письменности из системных шрифтов

Печать и экспорт в PDF

Печатает ядро: документ выводится в PDF/A-2b (meridian_doc_export_pdf), а PDF отдаётся ОС — на macOS системным окном печати (PDFKit, лист у окна), на Linux — в CUPS (lp, принтер по умолчанию), на Windows — командой «печать» программы для PDF. Вызов — ⌘P/Ctrl+P, палитра, меню «Файл», backstage «Печать»; раздел «Экспорт» сохраняет PDF рядом с документом и копирует путь. Проверено: сквозной тест (экспорт, путь, запрос печати); macOS — печать системой в файл (MERIDIAN_PRINT_TO, без окна): 7 страниц из 7, PDF от Quartz, страница совпадает с документом; Linux — CUPS в контейнере: без принтера понятная ошибка, с виртуальным PDF-принтером — 7 страниц.

Анимации по токенам

Длительности и кривые — из spec/design-tokens.json (duration.*, cubicBezier.*; генератор выдаёт их и в QML, и в Rust). Полоска под активной вкладкой ленты переезжает к новой за duration.base по кривой standard, палитра появляется со сдвигом 8 px за duration.fast (out), задержка подсказки — duration.tooltipDelay. Пока идёт переход, окно рисует кадр каждые 16 мс, потом перестаёт; при системной настройке «уменьшить движение» (macOS, Windows) переходы мгновенные. Проверено: модульные тесты кривых и переходов, сквозной (полоска в начале и в конце, сдвиг палитры 8 px), настоящее окно — палитра из меню за 5 кадров, затем покой.

Ввод через систему (IME) — macOS

Представление окна — клиент системы ввода текста (NSTextInputClient): обычные нажатия идут через interpretKeyEvents:, поэтому работают методы ввода (китайский, японский, корейский) и «мёртвые» клавиши (⌥E → «é»); черновик набора приходит оболочке событием ImePreedit и рисуется подчёркнутым, подтверждённый текст — обычным вводом; окно кандидатов ОС ставится у поля ввода (set_ime_area). Команды с ⌘ и служебные клавиши идут оболочке напрямую, пока нет черновика. Проверено на настоящем окне (--text-input): черновик «にほん» → подтверждение «日本» → синтетические нажатия «ab тест» через систему ввода — в палитре «日本ab тест»; черновик без подтверждения — ниже.

Черновик IME в палитре

На Linux текст идёт через метод ввода XIM (XOpenIM/XCreateIC, XFilterEvent, Xutf8LookupString): составные символы, «мёртвые» клавиши и IME (ibus, fcitx через XMODIFIERS). Проверено под Xvfb на sagr: раскладка us intl, xdotool key dead_acute e, dead_acute a, dead_diaeresis u → в палитре «éáü»; раскладка ru — «тёмн», как прежде.

Буфер обмена — свой на трёх ОС

macOS — NSPasteboard, Windows — Clipboard API (CF_UNICODETEXT), Linux — выделение CLIPBOARD X11: чтение через XConvertSelection, запись — владение выделением и ответы на запросы других программ (TARGETS, UTF8_STRING, STRING). Сейчас буфер работает в палитре (⌘V/Ctrl+V дописывает запрос) и в диалоге «Открыть» (вставленный путь к папке — переход, к файлу — открытие; ⌘C/Ctrl+C — путь выделенного); правка документа подключится к нему в фазе 1. Проверено: X11 под Xvfb — оболочка читает текст, положенный xclip, а xclip читает текст оболочки (кириллица и «✓»); macOS — чтение общего буфера совпадает с pbpaste, круг записи и чтения — на отдельном именованном буфере (MERIDIAN_PASTEBOARD), чтобы не трогать буфер пользователя; Windows — сборка и clippy.

Linux x86_64 — тот же кадр, снятый на сервере без экрана

Сборка в контейнере rust на сервере sagr (Linux x86_64) за 2 минуты; программа зависит только от libc, libm и libgcc_s — X11 и Wayland подгружаются при запуске, для сборки не нужны. Все 39 тестов проходят и там. Кадр светлой темы с Linux совпадает с кадром macOS arm64 на 99,92 % пикселей (остальное — сглаживание краёв на разном SIMD). Ниже — тёмная тема с палитрой команд, снятая на Linux.

Linux x86_64: тёмная тема и палитра

Что проверено

Проверка Результат
Тесты 27 модульных meridian-ui, 17 модульных и 27 сквозных meridian-write, 1 модульный meridian-platform — щелчки и клавиши как у пользователя (тема с ленты, палитра с клавиатуры, вкладки документов, масштаб, переход по страницам реального docx, сворачивание ленты и «подсмотр», список шрифтов, backstage и шаблоны, узкое окно, панели, пустое состояние, оглавление, свой диалог «Открыть», фокус с клавиатуры и его замыкание в палитре, подсказки клавиш с одно- и двухбуквенными кодами, свои кнопки окна, всплывающие подсказки с задержкой, стрелки по вкладкам и ленте, вставка и копирование, анимации, экспорт в PDF и печать, дерево доступности, черновик IME)
Настоящее окно macOS тема из системы, Retina 2×, фоновый рендер страниц ядра, перетаскивание за свой заголовок, двойной щелчок — развернуть; дымовой запуск just native-smoke
Linux x86_64 сборка за 2 мин в контейнере rust на сервере, 39 тестов, безоконные снимки; clippy -D warnings чистый; кадр совпадает с macOS на 99,92 % пикселей
Скорость кадр 2880×1800 на CPU — 7,4 мс медиана (just native-bench)
Размер один файл ≈ 14 МБ (macOS arm64) — интерфейс, шрифты и ядро
Лицензии cargo deny check — advisories, bans, licenses, sources: ok; Inter (OFL-1.1) — в third_party.toml, NOTICE и SBOM

Как запустить

just native-run docs/shell/samples/420596.docx
just native-shots

Параметры окна те же, что у Qt-оболочки: --theme light|dark|hc, --size 1440x900, --scene palette|backstage|collapsed|font-popup|open-dialog|focus|keytips|tooltip, --screenshot файл.png (безоконный снимок, работает и на сервере без экрана). Горячие клавиши — как в Qt-версии (⌘⇧P/Ctrl+Shift+P и Alt+Q — палитра, ⌘N, ⌘O, ⌘W, ⌘=/⌘-/⌘0, Ctrl+Tab, Ctrl+F1, ⌘⇧S), не зависят от раскладки.

Чего пока нет

Доступность на Windows (UI Automation) и текст документа для экранных чтецов, черновик IME на Windows и Linux, GPU-рендер, привязки Snap — план и оценки в own-shell.md §5. Правка документа появится вместе с моделью правки ядра (фаза 1), как и у Qt-прототипа.