From f480b291f1a707a704d6439288955ea63f0ec36e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 13:13:55 +0000 Subject: [PATCH] Draw a connect screen in the CJK faces only when it has wide glyphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every connect screen used --mono-cjk, whose Noto/MS Gothic/SimSun faces draw East Asian Ambiguous characters (block elements, box drawing) two cells wide. A reader without Cascadia Mono or Consolas falls through to those faces for Latin text too, so an ASCII banner built from █ and ▄ rendered every block at twice the width of the spaces around it. Measured in Chromium with Noto Sans Mono CJK SC at 12px: space 6px, █ 12px. Screens without a double-width rune now use --mono, and the figure gets a `wide` class when they have one. --mono gains Menlo, DejaVu Sans Mono and Liberation Mono, since ui-monospace and SFMono-Regular resolve only in Safari. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y8B9umJggwfAHzzLUPUyVj --- src/MUI.Web/Components/AnsiQuote.razor | 2 +- src/MUI.Web/wwwroot/app.css | 25 ++++++++++++++++++++----- tests/MUI.Web.Tests/Text/AnsiTests.cs | 21 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/MUI.Web/Components/AnsiQuote.razor b/src/MUI.Web/Components/AnsiQuote.razor index 7b45acdd..bc3fc5e4 100644 --- a/src/MUI.Web/Components/AnsiQuote.razor +++ b/src/MUI.Web/Components/AnsiQuote.razor @@ -28,7 +28,7 @@ break; default: -
+
@* role="img" prunes the art from the a11y tree like aria-hidden would, but unlike aria-hidden it leaves the region focusable — needed because it scrolls and a keyboard is how some readers scroll it. *@ diff --git a/src/MUI.Web/wwwroot/app.css b/src/MUI.Web/wwwroot/app.css index 1096bacd..e23fd4a8 100644 --- a/src/MUI.Web/wwwroot/app.css +++ b/src/MUI.Web/wwwroot/app.css @@ -92,7 +92,14 @@ --glow: 0 0 8px color-mix(in srgb, var(--accent) 55%, transparent); --sans: "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif; - --mono: "Cascadia Mono", ui-monospace, "SFMono-Regular", Consolas, monospace; + /* + Menlo, DejaVu Sans Mono and Liberation Mono are here for the block elements, not the letters. + `ui-monospace` and "SFMono-Regular" resolve only in Safari, so a Mac reader in Chrome or Firefox + without Cascadia or Consolas fell to the generic `monospace` (Courier), which has no █ or ▄, + and each block came from whatever face the browser found next. + */ + --mono: "Cascadia Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, "DejaVu Sans Mono", + "Liberation Mono", monospace; --kick: "JetBrains Mono", var(--mono); /* @@ -101,8 +108,15 @@ screen from a Chinese, Japanese or Korean game is column art built on the assumption that a Han glyph is exactly twice the advance of an ASCII one, which holds for Noto Sans Mono CJK, Sarasa Mono and MS Gothic and does NOT hold for PingFang, Microsoft YaHei or Hiragino Sans — those are - proportional, and falling into one turns pkuxkx's bordered banner into a ragged staircase. The - Latin faces stay in front so an ASCII screen is unchanged. + proportional, and falling into one turns pkuxkx's bordered banner into a ragged staircase. + + Only a screen that carries a double-width glyph is drawn in it (`figure.ansi.wide`). These + faces draw the East Asian Ambiguous characters — █ ▄ ▀ ░ and the box-drawing set — two cells + wide, which is what a GBK terminal does and what a CJK banner is drawn for, and exactly wrong + for an ASCII banner built out of blocks. A reader with none of the Latin faces in front (most + Linux desktops, which ship Noto Sans Mono CJK; a Mac with Office's MS Gothic) got every block + at twice the width of the spaces between them. Measured in Chromium against Noto Sans Mono + CJK SC at 12px: space 6px, █ 12px. No webfont, deliberately. Noto Sans Mono CJK SC is some eight megabytes a weight, and subsetting is per-screen — a different glyph set for every game, regenerated on every crawl. @@ -1650,9 +1664,10 @@ figure.ansi .quote { figure.ansi .quote:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } figure.ansi .quote pre { margin: 0; padding: 14px 16px; - font: var(--fs-sm)/1.4 var(--mono-cjk); font-variant-ligatures: none; + font: var(--fs-sm)/1.4 var(--mono); font-variant-ligatures: none; color: #aaaaaa; white-space: pre; } +figure.ansi.wide pre { font-family: var(--mono-cjk); } figure.ansi figcaption { display: flex; gap: 8px; flex-wrap: wrap; } /* @@ -1676,7 +1691,7 @@ figure.ansi figcaption { display: flex; gap: 8px; flex-wrap: wrap; } .screen-text > pre { margin-top: 8px; } .screen-text pre { margin: 0; - font: var(--fs-sm)/1.7 var(--mono-cjk); font-variant-ligatures: none; + font: var(--fs-sm)/1.7 var(--mono); font-variant-ligatures: none; color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere; diff --git a/tests/MUI.Web.Tests/Text/AnsiTests.cs b/tests/MUI.Web.Tests/Text/AnsiTests.cs index 45d6fa73..d00754b8 100644 --- a/tests/MUI.Web.Tests/Text/AnsiTests.cs +++ b/tests/MUI.Web.Tests/Text/AnsiTests.cs @@ -258,6 +258,27 @@ public async Task TheCaptionStatesTheWidestRowInCellsRatherThanAHalvedGrid() await Assert.That(html).DoesNotContain($"{Ansi.DefaultColumns / 2}×"); } + /// + /// Only a screen with a double-width glyph is drawn in the CJK faces. + /// + /// Those faces draw █ and ▄ two cells wide, so an ASCII banner built from blocks came out + /// with every block twice the width of the spaces beside it (mygame-5). + [Test] + public async Task OnlyAScreenWithWideGlyphsIsMarkedForTheCjkFaces() + { + var blocks = await Render.ComponentAsync(new() + { + ["Screen"] = Ansi.Parse(" ██ ██\n ▄▄▄▄▄▄\nconnect ", suppressedByOwner: false), + }); + var han = await Render.ComponentAsync(new() + { + ["Screen"] = Ansi.Parse("漢字\n漢字\n漢字", suppressedByOwner: false), + }); + + await Assert.That(blocks).Contains("
"); + await Assert.That(han).Contains("
"); + } + [Test] public async Task TheTextAlternativeCarriesNoColourCodes() {