Skip to content

fix(editor): alias Microsoft YaHei and YaHei UI as one family - #216

Open
atirna wants to merge 69 commits into
ZSeven-W:v0.8.5from
atirna:fix/windows-ui-font-family-split
Open

fix(editor): alias Microsoft YaHei and YaHei UI as one family#216
atirna wants to merge 69 commits into
ZSeven-W:v0.8.5from
atirna:fix/windows-ui-font-family-split

Conversation

@atirna

@atirna atirna commented Aug 16, 2026

Copy link
Copy Markdown

Summary

Windows ships msyh.ttc under two family names, Microsoft YaHei and Microsoft YaHei UI, and a system enumeration may surface only one spelling. Missing-font detection compared names with plain equality, so opening a design authored with the other spelling raised a false missing-font prompt.

This keeps an explicit same-file alias table for that pair only. Other Windows … UI faces (Segoe UI, Yu Gothic UI, Meiryo UI, Leelawadee UI) stay distinct, because those are different faces with different vertical metrics.

Changes

  • op-editor-core/font_catalog: WINDOWS_UI_FAMILY_ALIASES currently lists only microsoft yahei <-> microsoft yahei ui; is_same_font_family is ASCII-case-insensitive equality plus that table (no trailing- UI fold)
  • op-editor-core/missing_fonts: availability still uses that predicate, so a YaHei document is not missing when only YaHei UI is enumerated
  • op-editor-ui/missing_fonts_flow: supplying a YaHei UI file for a YaHei row does not warn; supplying Segoe UI for Segoe still does
  • op-editor-ui/property_panel_typography_paint: picker highlight follows the same alias table
  • op-host-web/web_fonts: local-font byte lookup may fall back to the YaHei alias key, not to an arbitrary … UI face

Related Issues

Fixes #211

Type

  • fix — Bug fix

Checklist

  • cargo test --workspace passes
  • cargo clippy --workspace --all-targets -- -D warnings passes
  • No unrelated changes included
  • Commit messages follow Conventional Commits

Kayshen-X and others added 30 commits August 13, 2026 21:38
Bring the iOS and Android touch shells to parity for canvas gestures, layer reordering, presentation controls, account access, and collaboration entry points.

Keep device authentication in Rust, restrict embedded login navigation, and fail closed unless mobile auth link inputs satisfy the build policy.
op_set_active_page gated its state update behind the editor feature, so
default-feature builds (cargo test --workspace, player shells) rebuilt the
scene on the old page and the switch was a silent no-op. Keep the session
state in lockstep with the editor host instead.
…host

The mobile-player work pushed both spines past the boundary script's
800-line ceiling. Tighten the new field docs instead of regrouping fields
(the existing substruct split was already measured and rejected beyond
PreviewState / SizeToggleState / DesignMdPanelState).
The builtin-catalog commit swapped several preset defaults (gpt-5.6,
doubao-seed-2-0-pro-260215, mimo-v2.5-pro, Qwen/Qwen3.5-35B-A3B) without
updating the reasoning-control classification sweep, which failed CI on
every platform. Each new id joins REASONING_CONTROL_WITHHELD with the same
no-verified-field reason as its predecessor.
The mobile compile guard checks op-engine-jni with the platform GPU
feature on every mobile target (metal on iOS, gl on Android), but the
crate only forwarded gl. Add the metal passthrough so the iOS legs of
the multi-platform workflow can check the JNI layer.
Writing a fake CLI to /tmp and spawning it back-to-back can hit
ETXTBSY (Text file busy) on CI runners' overlay filesystems. Retry a
Spawn failure with that message a bounded number of times so the probe
test keeps testing a real subprocess round-trip without flaking.
…e-image registry

The paint-time remote-image registry is process-global and drained once
per frame, so parallel harnesses in one test binary can steal each
other's recorded misses: a sibling's drain fires the upcall on its own
callback and the recording harness reads 0. Hold a frame lock so each
harness's enqueue-drain pair is atomic.
…o see its target

The inspector stack has grown past 800 px (compositing rows, size
toggles, per-corner radius), so the Effects section now sits below the
fold in an 800 px window and its popup dropped off-screen — the overlap
search found nothing. Raise the fixture window height so the Effects
popup lands over the Interactions rows exactly as the test intends.
Two mobile chrome runs (the top-bar document title and the page pill
counter) measured through the family-blind RenderBackend::measure_text,
which resolves the backend default instead of the system-ui family the
run is drawn in — under-reporting painted width on native. Route them
through text_metrics::measure_chrome / centered_text_x so the
text-measure boundary check stays green.
* fix(web): preserve account entry in managed embeds

* perf(canvas): reduce interaction latency

* test(canvas): split scene sync drag coverage

* test(canvas): keep drag tests within line cap

* refactor(canvas): keep widget host within line cap

* style(canvas): normalize widget host ending

---------

Co-authored-by: Fini <[email protected]>
* fix(web): preserve account entry in managed embeds

* fix(agent): stabilize design execution

---------

Co-authored-by: Fini <[email protected]>
Align the read-nodes CLI mapping with map_get/batch_get, which already
emit nodeIds as a JSON string array. Comma-joined positionals are split
client-side so `op read-nodes "n10,n11"` keeps resolving to two IDs.

@Kayshen-X Kayshen-X left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "… UI" suffix fold is too broad a rule for what it claims

The #211 diagnosis is right — msyh.ttc registers both Microsoft YaHei and Microsoft YaHei UI — but I don't think the mechanism is. is_same_font_family generalizes that single-file coincidence into a universal rule Name UI ≡ Name, applied symmetrically at every comparison site. For most Windows families that rule is false: the UI variant is a distinct face (often a distinct file) with intentionally different vertical metrics, and for a design tool that difference is a fidelity bug.

The predicate encodes a wrong general rule

Microsoft's own font list documents these as separate families:

  • Segoe (segoe.ttf) vs Segoe UI (segoeui.ttf) — different typefaces entirely
  • Yu Gothic vs Yu Gothic UI — different faces; the UI variant deliberately tightens line spacing
  • Meiryo vs Meiryo UI — different faces, different vertical metrics
  • Leelawadee vs Leelawadee UI — two separate files (leelawad.ttf / leelawui.ttf); Leelawadee UI is even in our own web fallback list (crates/op-host-web/src/web_fonts.rs:95)

Only Microsoft YaHeiMicrosoft YaHei UI shares one file — and even there they are two faces inside msyh.ttc with different line heights (the UI face is tighter).

Consequences at the call sites

  1. missing_fonts.rs (family_available) — a document authored with Yu Gothic on a machine that only has Yu Gothic UI is now silently "available" and renders with the wrong metrics. The old behavior was a noisy-but-true prompt; the new behavior is a silent fidelity bug.
  2. web_fonts.rs (font_data_key_for_family) — the fallback loads the wrong face's bytes: authored Segoe + enumerated-only Segoe UI registers the Segoe UI blob for Segoe. This is #211's mirror image — from "false missing-font prompt" to "silently loaded wrong font".
  3. missing_fonts_flow.rs (note_font_supplied) — suppressing the "File is X, not Y" note for all … UI pairs removes a real user safety signal: supplying a Segoe UI file for a missing Segoe row is a genuine mismatch and should still warn. Only the explicit known alias should be exempted here.
  4. property_panel_typography_paint.rs — the picker now highlights Segoe UI as the active row for a document authored Segoe, presenting a family the user never chose. Also, is_same_font_family allocates 1–2 Strings per comparison, in a per-frame per-row paint loop where the old eq_ignore_ascii_case was allocation-free.

Coverage is incomplete / inconsistent

The PR description says "every family-name comparison site", but:

  • command_font_replace.rs:199 still uses plain equality — replacing Microsoft YaHei won't touch nodes authored Microsoft YaHei UI (the same class of false negative as #211).
  • missing_fonts_flow.rs:197 (refresh_prompt reconciliation) and :224 now use two different equality rules in the same file.
  • Picker dedup sites (op-host-native/src/widget_host/font_picker_dispatch.rs:197, op-host-web/src/widget_host/web_fonts.rs:19) and font_catalog.rs:64 are untouched, so the picker still lists YaHei / YaHei UI as two rows while another module claims they are one family.
  • The tests assert the predicate's own definition but never assert !is_same_font_family("Yu Gothic", "Yu Gothic UI") — the case that would expose the over-breadth.

Suggested direction

The root issue is deciding availability by diffing an enumerated name list. Native already has the truth: op-host-native/src/backend/skia.rs enumerates via skia FontMgr (DirectWrite on Windows), and FontMgr::match_family_style("Microsoft YaHei") resolves straight to msyh.ttc. I'd rather see:

  • Native: ask the font manager whether the family resolves (or enumerate each face's full family-name set) instead of string-diffing names.
  • Web (where queryLocalFonts is name-only): an explicit, documented alias table scoped to the known same-file pairs, e.g.:
/// Windows pairs where one font FILE ships two family NAMES. Deliberately
/// not a general "… UI" fold: for most Windows families the UI variant
/// is a distinct face with different vertical metrics (Yu Gothic UI,
/// Meiryo UI, Leelawadee UI are all separate families).
pub const WINDOWS_UI_FAMILY_ALIASES: &[(&str, &str)] = &[
    ("microsoft yahei", "microsoft yahei ui"),
];

...and then route the mismatch-note exemption, picker highlight, and web byte fallback through that table (system families only — not user-imported fonts), leaving every other comparison site as-is.

If keeping a general predicate is preferred for the prompt UX, it should at minimum be scoped to system families, be one-directional (Name matches Name UI, not the reverse), and be renamed so the name doesn't promise "same face" (windows_ui_alias_equivalent or similar) — with the negative tests above added.

finiking and others added 3 commits August 16, 2026 12:50
The overflow-trigger fixtures sat at 88px, which lands within platform
font-metric slack of the 800px inner width: Windows CJK fallback
measured the title narrow enough that the pinned-shadow case never
crossed the threshold and CI went red while mac and linux stayed green.
Raise every trigger font size to 200px so even the most conservative
measurement backend proves the overflow; the negative cases gate on
form, pinning, or fixed-width and never depended on the exact size.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
…us revival

Sibling ornament chips drifted through arbitrary fill colors even after
structural equalisation, so same-position decorative fills join the
two-thirds majority vote (solid hexes and matching variable refs only;
mixed systems and gradients stay untouched). Card boards that outgrow
the portrait band after text wrapping now surface a format-drift
advisory instead of a silent ratio change — restoring 3:4 versus
keeping the long form is the caller's call. The ab-v3 corpus returns
from history into scripts/ab-v9 so the harness has a home again after
the TS retirement removed the original; audited baseline: 52/52 clean
under the geometry rubric.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
A trailing ' UI' fold treated Segoe/Yu Gothic/Meiryo/Leelawadee UI as
the same family as their non-UI faces. Keep only the documented
msyh.ttc pair (Microsoft YaHei ↔ Microsoft YaHei UI).
@atirna

atirna commented Aug 16, 2026

Copy link
Copy Markdown
Author

yeah the trailing UI fold was too broad. segoe / yu gothic / meiryo / leelawadee ui are different faces.

i swapped it for an explicit same-file alias table, currently just microsoft yahei <-> microsoft yahei ui. picker highlight, mismatch note, and the web byte lookup go through that. yu gothic vs yu gothic ui now stays missing (and supplying segoe ui for a segoe row still warns).

checked with cargo test -p op-editor-core --lib windows_ui plus yu_gothic_ui_does_not_satisfy_yu_gothic, the segoe mismatch-note test, and the web alias-key test.

thanks for catching that!

@atirna atirna changed the title fix(editor): treat the Windows '… UI' font-family split as one family fix(editor): alias Microsoft YaHei and YaHei UI as one family Aug 16, 2026
Kayshen-X and others added 16 commits August 16, 2026 16:07
The builtin chat provider only handed the model the mutating tools
(insert/update/move/delete_node) when the current selection was a pure
Frame set, so chatting right after opening a document — or with any
non-Frame node selected — silently degraded the agent to the three
read-only tools. Users experienced this as modification tools randomly
"dropping" and staying broken for whole documents.

Advertise the full CRUD set on every turn; mutation safety already
lives at the execution sink (collaboration gate + per-tool
validation), so hiding definitions only created confusion. Removes the
now-unused write-scope filter and frame-target probe.

Closes ZSeven-W#209
Mobile editors previously dropped every export/save file action at the
FFI shell-action drain, so tapping Export did nothing on iOS/Android.

- Extract the shared raster/SVG/PDF renderer from op-host-services into
  the mobile-safe op-render-export crate (same scene painter as the
  desktop export path; op-host-services re-exports keep its API).
- op-engine-ffi: freeze one export artifact per request and add the
  SHELL_ACTION_EXPORT_DOCUMENT bridge (copy file name, write once to a
  shell-owned staging path, cancel) so large payloads never cross the
  ABI as a second copy.
- iOS: DocumentExportCoordinator stages the file and presents the
  system Files save picker.
- Android: JNI bindings plus a SAF create-document flow — Rust writes
  the app-private staging file, the chosen destination receives a
  plain copy, and every terminal path removes the staging directory.
- WebP is hidden on mobile because the pinned Skia archives ship
  without its encoder; PNG/JPEG/SVG/PDF stay available everywhere.
- The full editor now starts on a canonical untitled document instead
  of the bundled demo, with a localized untitled app-bar title and a
  New File action in the mobile more panel.
HTML imports map visibility:hidden elements to visible:false nodes, and
the old every-descendant-visible gate made every imported frame
permanently undeletable, undraggable and unreorderable. Locks anywhere
in the subtree still protect; visibility no longer does, on the selected
root or its descendants (Figma parity).
elementFromPoint stops at a shadow host, which made everything inside a
web-component sub-app (wujie, qiankun strictStyleIsolation) unpickable.
Descend like DevTools inspect does, but stop at slot-bearing shadow
roots: slotted light children are only captured through the host.
The extractor's 20,000-node budget drained depth-first, so on large
pages everything late in DOM order — micro-frontend sub-app containers
above all — was silently dropped whole. Double the node budget and lift
the local byte chain (client precheck, ingest route, importer) from 32
to 48 MiB in lockstep, still under the endpoint-wide 64 MiB body cap.

The hub inbox keeps its own 32 MiB server contract: its cap is now a
separate constant with a dedicated oversize message, since a 32-48 MiB
capture imports locally but cannot reach the account. The /mcp fallback
envelope gets its own precheck (JSON re-escaping can inflate past the
raw-snapshot cap), the request timeout no longer ties the editor's 15 s
ack budget, and the measure cache is sized for 40k text leaves.
…r, and locale

Region-aware auth configure (cn/global), begin-login/sign-out/account-snapshot
FFI + JNI, one-shot pending flags for the native login / account center /
language picker, runtime locale switching, safe-area band painting that blends
with the app bar and dock, and a Language entry in the mobile more panel.
…cale UI

Replaces the login WebView on iOS and Android with fully native screens in
the ZSeven web design: labeled boxed inputs with lucide icons, gradient
primary button, region-accurate provider cards fetched from the pairing
origin, native register / password-reset with live password rules, a native
account center, a 15-language picker, IP-informed cn/global region resolve
with persisted override, and an iOS GitHub-releases update check.
Provider cards now open the pairing login page in an in-app Safari sheet
(iOS) / Chrome Custom Tab (Android) instead of bouncing to the external
browser, carrying a provider deep-link parameter for the tapped card. This
is the interim path while per-provider native SDK sign-in lands.
The Apple provider card runs the system AuthenticationServices sheet: a
fresh 32-byte base64url nonce binds the request, the identity token is
exchanged at the SSO's new POST /api/v1/auth/providers/apple/native-login
into the screen's cookie jar, and the running device pairing is approved
directly — no web page involved. A user cancel returns to the screen and a
sheet failure surfaces an inline error. Adds the Sign in with Apple
entitlement plus SafariServices/AuthenticationServices links.
Generation quality from weaker models swings hard between runs on the
same prompt. `OPENPENCIL_LLM_TEMPERATURE` (0.0..=2.0) lets a run pin the
sampling temperature so the variance source itself can be measured;
unset or unparsable leaves the field off the request body entirely, so
provider defaults and existing behaviour are untouched. Wired in both
openai-compat bodies: the smoke harness client and the builtin HTTP chat
path shared by every model that rides it.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
A single generation from a weaker model is close to a dice roll: across
18 candidates in a three-prompt trial, 8 carried geometry defects. Rather
than fight that variance, harvest it — `OPENPENCIL_SMOKE_BEST_OF=N`
(2..=4) runs N independent generations from clones of the prepared base
state, scores each against the real-layout geometry diagnostics, and
keeps the winner: fewest issues, then most nodes as a richness proxy,
then first to arrive. Every candidate is also written to
`<out>.cand<i>.op` so a human can compare, and a candidate whose
generation errors scores a sentinel instead of aborting the rest.

Selection, scoring and saving live in best_of.rs with unit tests over the
ordering rule; main.rs keeps only the branch, and the single-run path is
untouched when the knob is unset.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
Non-Apple provider cards now open the SSO start endpoint (channel
web_mobile) with the device pairing attached, so the in-app sheet lands
directly on the provider's authorize screen and returns to the dedicated
pairing-approval page — the ZSeven login web page no longer appears in the
flow. Requires the SSO backend with device_pairing start support.
atirna and others added 2 commits August 16, 2026 22:43
…-family-split

# Conflicts:
#	.github/workflows/ios-app-store.yml
#	.github/workflows/version-sync.yml
#	README.md
#	crates/op-engine-ffi/src/editor.rs
#	crates/op-engine-ffi/tests/editor.rs
#	scripts/build-android-release.sh
#	scripts/build-mobile-auth-dev.sh
#	scripts/prepare-ios-app-store-runner.sh
#	scripts/publish-ios-testflight.sh
#	scripts/sync-version.test.sh
#	tools/check-android-release-workflow.sh
#	tools/check-android-release-workflow.test.sh
#	tools/check-ios-app-store-workflow.sh
#	tools/check-version-sync.sh
#	tools/check-version-sync.test.sh
@atirna
atirna force-pushed the fix/windows-ui-font-family-split branch from 3368ecc to 03bd531 Compare August 17, 2026 05:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

【字体】获取字体的地方有bug

5 participants