feat: shape and antialias Tessera text with system fonts
Verify Tessera GUI / verify (push) Successful in 13m28s
Verify Tessera GUI / verify (push) Successful in 13m28s
This commit is contained in:
@@ -13,3 +13,7 @@ through Aster's Interactive interface. Function-entry compatibility, original
|
||||
source positions, page methods and page budgets are also checked. The workflow
|
||||
checks the macOS packaging script's syntax without running it on Linux.
|
||||
Docker is used only in this remote workflow; local compilation is native.
|
||||
|
||||
The test image installs DejaVu Sans/Mono and Noto CJK for font metrics and Chinese
|
||||
fallback checks. Text regression tests inspect glyph selection, measured advances,
|
||||
fractional scale, multiline bounds, Unicode truncation and antialiased opacity.
|
||||
|
||||
Generated
+239
-5
@@ -275,6 +275,39 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core_maths"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-text"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"fontdb",
|
||||
"harfrust",
|
||||
"linebender_resource_handle",
|
||||
"log",
|
||||
"rangemap",
|
||||
"rustc-hash",
|
||||
"self_cell",
|
||||
"skrifa 0.40.0",
|
||||
"smol_str 0.3.6",
|
||||
"swash",
|
||||
"sys-locale",
|
||||
"unicode-bidi",
|
||||
"unicode-linebreak",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.23"
|
||||
@@ -408,10 +441,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[package]]
|
||||
name = "font8x8"
|
||||
version = "0.3.1"
|
||||
name = "font-types"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e"
|
||||
checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "font-types"
|
||||
version = "0.12.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fontconfig-parser"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
|
||||
dependencies = [
|
||||
"roxmltree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fontdb"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"
|
||||
dependencies = [
|
||||
"fontconfig-parser",
|
||||
"log",
|
||||
"memmap2",
|
||||
"slotmap",
|
||||
"tinyvec",
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
@@ -506,6 +574,19 @@ dependencies = [
|
||||
"r-efi 6.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "harfrust"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"bytemuck",
|
||||
"core_maths",
|
||||
"read-fonts 0.37.0",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
@@ -635,6 +716,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.25"
|
||||
@@ -647,6 +734,12 @@ dependencies = [
|
||||
"redox_syscall 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linebender_resource_handle"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@@ -1170,12 +1263,40 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rangemap"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d"
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"core_maths",
|
||||
"font-types 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"font-types 0.12.7",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
@@ -1203,6 +1324,18 @@ dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -1272,6 +1405,12 @@ dependencies = [
|
||||
"tiny-skia",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
@@ -1343,12 +1482,41 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "skrifa"
|
||||
version = "0.40.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"read-fonts 0.37.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "skrifa"
|
||||
version = "0.44.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"read-fonts 0.41.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "slotmap"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.2"
|
||||
@@ -1389,6 +1557,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smol_str"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523"
|
||||
|
||||
[[package]]
|
||||
name = "softbuffer"
|
||||
version = "0.4.8"
|
||||
@@ -1427,6 +1601,17 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||
|
||||
[[package]]
|
||||
name = "swash"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e"
|
||||
dependencies = [
|
||||
"skrifa 0.44.0",
|
||||
"yazi",
|
||||
"zeno",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
@@ -1449,6 +1634,15 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tessera"
|
||||
version = "0.1.0"
|
||||
@@ -1456,12 +1650,13 @@ dependencies = [
|
||||
"anyhow",
|
||||
"compiler",
|
||||
"core",
|
||||
"font8x8",
|
||||
"cosmic-text",
|
||||
"frontend",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"softbuffer",
|
||||
"tiny_http",
|
||||
"unicode-segmentation",
|
||||
"winit",
|
||||
]
|
||||
|
||||
@@ -1555,6 +1750,12 @@ dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.2"
|
||||
@@ -1615,6 +1816,15 @@ name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||
dependencies = [
|
||||
"core_maths",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
@@ -1622,6 +1832,18 @@ version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-script"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.13.3"
|
||||
@@ -1979,7 +2201,7 @@ dependencies = [
|
||||
"rustix 0.38.44",
|
||||
"sctk-adwaita",
|
||||
"smithay-client-toolkit",
|
||||
"smol_str",
|
||||
"smol_str 0.2.2",
|
||||
"tracing",
|
||||
"unicode-segmentation",
|
||||
"wasm-bindgen",
|
||||
@@ -2068,6 +2290,18 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "yazi"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5"
|
||||
|
||||
[[package]]
|
||||
name = "zeno"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.59"
|
||||
|
||||
+2
-1
@@ -9,13 +9,14 @@ license = "MIT OR Apache-2.0"
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
compiler = { path = "vendor/aster/crates/compiler" }
|
||||
cosmic-text = "0.19"
|
||||
endor_core = { package = "core", path = "vendor/aster/crates/core" }
|
||||
font8x8 = "0.3"
|
||||
frontend = { path = "vendor/aster/crates/frontend" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
softbuffer = "0.4.8"
|
||||
tiny_http = "0.12"
|
||||
unicode-segmentation = "1.12"
|
||||
winit = "0.30.13"
|
||||
|
||||
[profile.release]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
FROM rust:1.91.1-bookworm
|
||||
WORKDIR /build
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libx11-dev libxkbcommon-dev libwayland-dev libudev-dev libegl1-mesa-dev libgbm-dev \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libx11-dev libxkbcommon-dev libwayland-dev libudev-dev libegl1-mesa-dev libgbm-dev fonts-dejavu-core fonts-noto-cjk \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rustup component add rustfmt clippy
|
||||
ENV CARGO_BUILD_JOBS=1 CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0
|
||||
|
||||
@@ -32,7 +32,9 @@ cargo build --release --locked
|
||||
|
||||
Cargo compiles the required Aster libraries together with Tessera. No separate
|
||||
Aster build, service or deployment is required. Local builds download dependencies
|
||||
from official crates.io; do not substitute a mirror. Docker and automated tests
|
||||
from official crates.io by default. A temporary dependency mirror may be used
|
||||
when the official source is unavailable, without changing global configuration.
|
||||
Docker and automated tests
|
||||
are not used locally. The command above produces a native executable. To create a
|
||||
Finder-launchable development app on macOS, run:
|
||||
|
||||
@@ -113,7 +115,15 @@ more than once, its final successful render selects the visible root. Rendering
|
||||
does not publish partial execution: a later error rejects the entire submission.
|
||||
|
||||
Aster keeps its exact numeric representation; conversion occurs only at the UI
|
||||
boundary. Text uses the current bitmap renderer, with Chinese fonts still pending.
|
||||
boundary. Text is shaped and antialiased with COSMIC Text and Swash, using
|
||||
installed system fonts and per-glyph fallback (including Chinese when a CJK font
|
||||
is installed). UI text uses a proportional sans-serif font; developer source uses
|
||||
a monospace font. The existing size argument remains a 1..8 multiplier: size 1
|
||||
is 14 logical pixels with a 20-pixel line height; size 2 is 28/40. Row/Column layout
|
||||
uses measured text advances and multiline height. Device scale remains fractional,
|
||||
and glyphs are rasterized at the actual display resolution rather than enlarged.
|
||||
The application does not bundle proprietary OS fonts. Linux installations need
|
||||
fonts such as DejaVu Sans/Mono and Noto CJK; the remote test image installs these.
|
||||
|
||||
## Candidate execution and publication
|
||||
|
||||
@@ -162,7 +172,8 @@ source budgets and VM instruction limits are not a complete untrusted-code sandb
|
||||
Toggle the sidebar with Command-Option-I or F12 on macOS, Control-Shift-I or F12
|
||||
on Windows/Linux. It displays Aster AST functions, bindings, calls and expressions.
|
||||
Selection shows their ranges in the exact submitted source. Tessera does not
|
||||
maintain a second language AST.
|
||||
maintain a second language AST. Source lines and tree labels are fitted to the
|
||||
available measured width; truncation preserves Unicode grapheme clusters.
|
||||
|
||||
## Validation
|
||||
|
||||
@@ -174,4 +185,6 @@ The API regressions use actual HTTP requests, including DELETE while the worker
|
||||
is occupied and a queued PUT is superseded.
|
||||
Linux CI does not verify macOS window presentation. Aster has no standalone CI/CD; Lyra CI
|
||||
explicitly validates the pinned engine itself. CI uses official Cargo sources
|
||||
and standard HTTPS checkout without custom SSH secrets.
|
||||
and standard HTTPS checkout without custom SSH secrets. Font checks cover
|
||||
proportional/monospace advances, fractional scaling, multiline height, Chinese
|
||||
fallback, grapheme-safe truncation, antialiasing and alpha blending.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
| [page.endor](page.endor) | Four top-level statements construct text, a rectangle and a page, then call page.render; no main wrapper. |
|
||||
| [main.endor](main.endor) | The earlier function-entry form, producing the same frame as page.endor. |
|
||||
| [hello.endor](hello.endor) | Richer function-entry example covering a helper, Row/Column and Spacer. |
|
||||
| [fonts.endor](fonts.endor) | Mixed Chinese/Latin text and proportional widths, with rectangles following narrow and wide letters. |
|
||||
|
||||
Submit a file as the UTF-8 body of a namespace PUT request. No namespace,
|
||||
scene, render block or imports belong in the source.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
let title = ui.text("Hello Endor · 你好,Tessera", 2, "#242422")
|
||||
let detail = ui.text("中文 English 123 — café", 1, "#606060")
|
||||
let row = ui.row(0, 12, ui.text("iiii"), ui.rect(24, 20, 4, "#DCE8FF"), ui.text("WWWW"), ui.rect(24, 20, 4, "#DCE8FF"))
|
||||
let page = ui.page()
|
||||
page.render(ui.column(24, 12, title, detail, row))
|
||||
+5
-4
@@ -1,10 +1,11 @@
|
||||
# Native packaging
|
||||
|
||||
`build-macos-app.sh` builds the release executable with the normal Cargo lockfile
|
||||
and creates `target/release/Tessera.app`. It uses the local Rust toolchain and
|
||||
official Cargo sources; no mirror or Docker configuration is installed, and no
|
||||
automated tests run. `CARGO_NET_OFFLINE=true` can reuse an already populated
|
||||
official dependency cache.
|
||||
and creates `target/release/Tessera.app`. It uses the local Rust toolchain;
|
||||
no mirror or Docker configuration is installed, and no automated tests run.
|
||||
Additional arguments are forwarded to Cargo, such as `--offline` to reuse an
|
||||
already populated cache or temporary `--config` options for a dependency mirror
|
||||
when official crates.io is unavailable. Defaults remain the official source.
|
||||
|
||||
The local bundle has identifier `com.entroia.tessera` and can be opened from Finder.
|
||||
It is a development app, without a distribution signature or notarization.
|
||||
|
||||
@@ -6,7 +6,7 @@ if [ "$(uname -s)" != Darwin ]; then
|
||||
exit 1
|
||||
fi
|
||||
cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
cargo build --release --locked
|
||||
cargo build --release --locked "$@"
|
||||
version=$(awk -F '"' '$1 ~ /^version[[:space:]]*=/ { print $2; exit }' Cargo.toml)
|
||||
test -n "$version"
|
||||
bundle="$(pwd)/target/release/Tessera.app"
|
||||
|
||||
+7
-1
@@ -15,6 +15,7 @@
|
||||
| display_list.rs | Rendering primitives and color representation |
|
||||
| geometry.rs | Native points and rectangles |
|
||||
| software_renderer.rs | Software rasterization of the display list |
|
||||
| text.rs | Shared system font discovery, shaping, real measurements, antialiased glyphs and bounded caches |
|
||||
| README.md | Module map |
|
||||
|
||||
Aster is an unmodified source submodule. Language parsing, AST definitions,
|
||||
@@ -37,4 +38,9 @@ Tests in api.rs execute real HTTP CRUD and a controlled queued-PUT/DELETE race
|
||||
without opening a window. Tests in endor_host.rs cover lifecycle, direct script and
|
||||
module entry, page methods, multiline inputs, original source ranges, malformed input,
|
||||
entry checks, budgets, failure preservation and stale-result invalidation. They
|
||||
run only in remote CI. See the [project guide](../README.md) for public behavior.
|
||||
run only in remote CI. Font tests in text.rs and software_renderer.rs cover real
|
||||
advances, multiline height, fractional scaling, CJK fallback, grapheme-safe fitting,
|
||||
antialiasing and transparency. A process-wide mutex keeps font discovery, shaping
|
||||
and glyph caches shared between the worker's layout and window redraw. The
|
||||
256-string/512-KiB source cache and 4096-glyph/32-MiB image cache are discarded
|
||||
when their budgets are exceeded. See the [project guide](../README.md) for public behavior.
|
||||
|
||||
+4
-2
@@ -339,8 +339,10 @@ PUT waits for its worker result. Errors return 422 with diagnostics and preserve
|
||||
the last successful revision. A newer accepted PUT or DELETE supersedes older
|
||||
work (409); a full eight-entry queue returns 503. DELETE immediately clears the
|
||||
document. GET remains available while Aster executes. The window redraws from an
|
||||
owned snapshot; resize does not rerun Endor. This first stage builds static UI;
|
||||
events, persistent VM state, Chinese font rendering and process isolation remain
|
||||
owned snapshot; resize does not rerun Endor. Text uses system fonts, antialiasing
|
||||
and font fallback; Chinese needs an installed CJK font. Size 1 is 14 logical pixels
|
||||
with 20-pixel line height; larger sizes multiply both. This first stage builds static UI;
|
||||
events, persistent VM state and process isolation remain
|
||||
future work. It is not an untrusted-code sandbox.
|
||||
"##,
|
||||
name = namespace.name,
|
||||
|
||||
+14
-25
@@ -2,6 +2,7 @@ use crate::{
|
||||
display_list::{Color, DisplayList, DrawCommand},
|
||||
document::{CodeNode, RenderDocument},
|
||||
geometry::{Point, Rect},
|
||||
text::{self as typography, TextStyle},
|
||||
};
|
||||
|
||||
const PANEL: Color = Color::rgb(247, 247, 245);
|
||||
@@ -154,14 +155,13 @@ pub fn build(
|
||||
if state.selected_node_id.as_deref() == Some(node.id.as_str()) {
|
||||
fill(&mut list, row, 5.0 * scale, SELECTED);
|
||||
}
|
||||
text(
|
||||
&mut list,
|
||||
row.x + (9.0 + depth as f32 * 14.0) * scale,
|
||||
row.y + 8.0 * scale,
|
||||
&fit(&format!("{} {}", node.kind, node.name), 44),
|
||||
scale,
|
||||
TEXT,
|
||||
let text_x = row.x + (9.0 + depth as f32 * 14.0) * scale;
|
||||
let label = typography::fit(
|
||||
&format!("{} {}", node.kind, node.name),
|
||||
(width - 14.0 * scale - text_x).max(0.0),
|
||||
TextStyle::code().scaled(scale),
|
||||
);
|
||||
text(&mut list, text_x, row.y + 3.0 * scale, &label, scale, TEXT);
|
||||
layout.rows.push(TreeRowHit {
|
||||
rect: row,
|
||||
node_id: node.id.clone(),
|
||||
@@ -235,7 +235,7 @@ fn draw_source(
|
||||
let first_y = area.y + 42.0 * scale;
|
||||
let maximum_lines =
|
||||
((area.y + area.height - first_y - 10.0 * scale) / line_height).max(0.0) as usize;
|
||||
let max_chars = ((area.width / (8.0 * scale)) - 9.0).max(8.0) as usize;
|
||||
let source_width = (area.width - 65.0 * scale).max(0.0);
|
||||
|
||||
for (offset, source_line) in all_lines[start..end].iter().take(maximum_lines).enumerate() {
|
||||
let line_number = start + offset + 1;
|
||||
@@ -252,7 +252,11 @@ fn draw_source(
|
||||
list,
|
||||
area.x + 55.0 * scale,
|
||||
y,
|
||||
&fit(&source_line.replace('\t', " "), max_chars),
|
||||
&typography::fit(
|
||||
&source_line.replace('\t', " "),
|
||||
source_width,
|
||||
TextStyle::code().scaled(scale),
|
||||
),
|
||||
scale,
|
||||
TEXT,
|
||||
);
|
||||
@@ -266,21 +270,6 @@ fn flatten<'a>(node: &'a CodeNode, depth: usize, output: &mut Vec<(usize, &'a Co
|
||||
}
|
||||
}
|
||||
|
||||
fn fit(value: &str, maximum: usize) -> String {
|
||||
let characters: Vec<_> = value.chars().collect();
|
||||
if characters.len() <= maximum {
|
||||
return value.to_owned();
|
||||
}
|
||||
if maximum < 3 {
|
||||
return characters.into_iter().take(maximum).collect();
|
||||
}
|
||||
characters
|
||||
.into_iter()
|
||||
.take(maximum - 3)
|
||||
.chain(['.', '.', '.'])
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn fill(list: &mut DisplayList, rect: Rect, radius: f32, color: Color) {
|
||||
list.push(DrawCommand::FillRoundedRect {
|
||||
rect,
|
||||
@@ -293,7 +282,7 @@ fn text(list: &mut DisplayList, x: f32, y: f32, value: &str, scale: f32, color:
|
||||
list.push(DrawCommand::Text {
|
||||
origin: Point::new(x, y),
|
||||
value: value.to_owned(),
|
||||
scale: scale.round().max(1.0) as u32,
|
||||
style: TextStyle::code().scaled(scale),
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
use crate::geometry::{Point, Rect};
|
||||
use crate::text::TextStyle;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Color {
|
||||
@@ -45,7 +46,7 @@ pub enum DrawCommand {
|
||||
Text {
|
||||
origin: Point,
|
||||
value: String,
|
||||
scale: u32,
|
||||
style: TextStyle,
|
||||
color: Color,
|
||||
},
|
||||
}
|
||||
|
||||
+1
-1
@@ -300,7 +300,7 @@ impl RenderCommand {
|
||||
} => DrawCommand::Text {
|
||||
origin: Point::new(x * scale, y * scale),
|
||||
value: value.clone(),
|
||||
scale: ((*size as f32 * scale).round() as u32).max(1),
|
||||
style: crate::text::TextStyle::ui(*size).scaled(scale),
|
||||
color: parse_color(color)?,
|
||||
},
|
||||
})
|
||||
|
||||
+6
-12
@@ -378,7 +378,7 @@ mod tests {
|
||||
matches!(&document.frame.commands[0], RenderCommand::Text { value, size: 2, x: 24.0, y: 24.0, .. } if value == "Hello Endor")
|
||||
);
|
||||
assert!(
|
||||
matches!(&document.frame.commands[1], RenderCommand::RoundedRect { x: 24.0, y: 52.0, width: 240.0, height: 80.0, radius: 12.0, color } if color == "#DCE8FF")
|
||||
matches!(&document.frame.commands[1], RenderCommand::RoundedRect { x: 24.0, y: 76.0, width: 240.0, height: 80.0, radius: 12.0, color } if color == "#DCE8FF")
|
||||
);
|
||||
assert_eq!(document.display_list(2.0).unwrap().commands().len(), 3);
|
||||
let wrapped = compile(include_str!("../examples/main.endor"), "app.main", 10).unwrap();
|
||||
@@ -553,19 +553,16 @@ paint(ui.text("last"))"#;
|
||||
&document.frame.commands[1],
|
||||
RenderCommand::RoundedRect {
|
||||
x: 24.0,
|
||||
y: 52.0,
|
||||
y: 76.0,
|
||||
width: 240.0,
|
||||
height: 80.0,
|
||||
..
|
||||
}
|
||||
));
|
||||
let first_width = crate::text::measure("Aster VM", crate::text::TextStyle::ui(1)).width;
|
||||
assert!(matches!(
|
||||
&document.frame.commands[3],
|
||||
RenderCommand::Text {
|
||||
x: 120.0,
|
||||
y: 144.0,
|
||||
..
|
||||
}
|
||||
RenderCommand::Text { x, y: 168.0, .. } if (*x - (24.0 + first_width + 12.0 + 8.0 + 12.0)).abs() < 0.01
|
||||
));
|
||||
// Candidate::close already proved zero remaining VM objects. Both
|
||||
// redraws must still work from native data at different display scales.
|
||||
@@ -583,13 +580,10 @@ function main(): View { let child = ui.text("shared") return wrap(child) }"#;
|
||||
&document.frame.commands[0],
|
||||
RenderCommand::Text { x: 4.0, y: 4.0, .. }
|
||||
));
|
||||
let width = crate::text::measure("shared", crate::text::TextStyle::ui(1)).width;
|
||||
assert!(matches!(
|
||||
&document.frame.commands[1],
|
||||
RenderCommand::Text {
|
||||
x: 60.0,
|
||||
y: 4.0,
|
||||
..
|
||||
}
|
||||
RenderCommand::Text { x, y: 4.0, .. } if (*x - (4.0 + width + 8.0)).abs() < 0.01
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -3,6 +3,7 @@ use anyhow::{Result, bail};
|
||||
|
||||
use crate::{
|
||||
document::{Frame, RenderCommand},
|
||||
text::{self, TextStyle},
|
||||
ui_bridge::{MAX_DIMENSION, UiKind, UiNode},
|
||||
};
|
||||
|
||||
@@ -50,9 +51,10 @@ fn layout(
|
||||
size: *size,
|
||||
color: color.clone(),
|
||||
});
|
||||
let measured = text::measure(value, TextStyle::ui(*size));
|
||||
Size {
|
||||
width: value.chars().count() as f32 * 8.0 * *size as f32,
|
||||
height: 8.0 * *size as f32,
|
||||
width: measured.width,
|
||||
height: measured.height,
|
||||
}
|
||||
}
|
||||
UiKind::Rect {
|
||||
|
||||
@@ -9,6 +9,7 @@ mod geometry;
|
||||
mod layout;
|
||||
mod scene;
|
||||
mod software_renderer;
|
||||
mod text;
|
||||
mod ui_bridge;
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
+37
-35
@@ -1,8 +1,7 @@
|
||||
use font8x8::{BASIC_FONTS, UnicodeFonts};
|
||||
|
||||
use crate::{
|
||||
display_list::{Color, DisplayList, DrawCommand},
|
||||
geometry::{Point, Rect},
|
||||
text::{self, TextStyle},
|
||||
};
|
||||
|
||||
pub struct SoftwareRenderer;
|
||||
@@ -32,9 +31,9 @@ impl SoftwareRenderer {
|
||||
DrawCommand::Text {
|
||||
origin,
|
||||
value,
|
||||
scale,
|
||||
style,
|
||||
color,
|
||||
} => canvas.draw_text(*origin, value, *scale, *color),
|
||||
} => canvas.draw_text(*origin, value, *style, *color),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,29 +87,12 @@ impl Canvas<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_text(&mut self, origin: Point, value: &str, scale: u32, color: Color) {
|
||||
let scale = scale.max(1) as i32;
|
||||
let mut cursor_x = origin.x.round() as i32;
|
||||
let baseline_y = origin.y.round() as i32;
|
||||
|
||||
for character in value.chars() {
|
||||
if let Some(glyph) = BASIC_FONTS.get(character) {
|
||||
for (row, byte) in glyph.iter().enumerate() {
|
||||
for column in 0..8 {
|
||||
if byte & (1 << column) != 0 {
|
||||
self.fill_pixels(
|
||||
cursor_x + column * scale,
|
||||
baseline_y + row as i32 * scale,
|
||||
scale,
|
||||
scale,
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cursor_x += 8 * scale;
|
||||
}
|
||||
fn draw_text(&mut self, origin: Point, value: &str, style: TextStyle, color: Color) {
|
||||
let x = origin.x.round() as i32;
|
||||
let y = origin.y.round() as i32;
|
||||
text::draw(value, style, color, |dx, dy, pixel| {
|
||||
self.blend_pixel(x + dx, y + dy, pixel)
|
||||
});
|
||||
}
|
||||
|
||||
fn clamped_bounds(&self, rect: Rect) -> (i32, i32, i32, i32) {
|
||||
@@ -122,14 +104,6 @@ impl Canvas<'_> {
|
||||
)
|
||||
}
|
||||
|
||||
fn fill_pixels(&mut self, x: i32, y: i32, width: i32, height: i32, color: Color) {
|
||||
for py in y.max(0)..(y + height).min(self.height as i32) {
|
||||
for px in x.max(0)..(x + width).min(self.width as i32) {
|
||||
self.blend_pixel(px, py, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn blend_pixel(&mut self, x: i32, y: i32, source: Color) {
|
||||
if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 || source.alpha == 0
|
||||
{
|
||||
@@ -192,4 +166,32 @@ mod tests {
|
||||
assert_eq!(pixels[5], 0x00ff0000);
|
||||
assert_eq!(pixels[0], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_edges_are_antialiased_and_respect_opacity() {
|
||||
let mut list = DisplayList::default();
|
||||
list.push(DrawCommand::Clear(Color::rgb(0, 0, 0)));
|
||||
list.push(DrawCommand::Text {
|
||||
origin: Point::new(2.0, 2.0),
|
||||
value: "Hello 中文".into(),
|
||||
style: TextStyle::ui(1).scaled(1.25),
|
||||
color: Color::rgba(255, 255, 255, 128),
|
||||
});
|
||||
let mut pixels = vec![0; 256 * 48];
|
||||
SoftwareRenderer::render(&list, &mut pixels, 256, 48);
|
||||
assert!(pixels.iter().any(|pixel| *pixel > 0));
|
||||
assert!(pixels.iter().all(|pixel| (pixel & 255) <= 128));
|
||||
assert!(pixels.iter().any(|pixel| (1..128).contains(&(pixel & 255))));
|
||||
|
||||
let mut invisible = DisplayList::default();
|
||||
invisible.push(DrawCommand::Text {
|
||||
origin: Point::new(2.0, 2.0),
|
||||
value: "Hello 中文".into(),
|
||||
style: TextStyle::ui(1),
|
||||
color: Color::rgba(255, 255, 255, 0),
|
||||
});
|
||||
let mut pixels = vec![0; 256 * 48];
|
||||
SoftwareRenderer::render(&invisible, &mut pixels, 256, 48);
|
||||
assert!(pixels.iter().all(|pixel| *pixel == 0));
|
||||
}
|
||||
}
|
||||
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
//! Shared font discovery, shaping, measurement and antialiased rasterization.
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use cosmic_text::{Align, Attrs, Buffer, Family, FontSystem, Metrics, Shaping, SwashCache, Wrap};
|
||||
|
||||
use crate::display_list::Color;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub enum TextFamily {
|
||||
Interface,
|
||||
Code,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct TextStyle {
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
pub family: TextFamily,
|
||||
}
|
||||
|
||||
impl TextStyle {
|
||||
pub fn ui(size: u32) -> Self {
|
||||
Self {
|
||||
font_size: 14.0 * size as f32,
|
||||
line_height: 20.0 * size as f32,
|
||||
family: TextFamily::Interface,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn code() -> Self {
|
||||
Self {
|
||||
font_size: 13.0,
|
||||
line_height: 18.0,
|
||||
family: TextFamily::Code,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scaled(self, scale: f32) -> Self {
|
||||
Self {
|
||||
font_size: self.font_size * scale,
|
||||
line_height: self.line_height * scale,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct TextSize {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||
struct TextKey {
|
||||
value: String,
|
||||
font_size: u32,
|
||||
line_height: u32,
|
||||
family: TextFamily,
|
||||
}
|
||||
|
||||
struct PreparedText {
|
||||
buffer: Buffer,
|
||||
size: TextSize,
|
||||
}
|
||||
|
||||
struct TextEngine {
|
||||
fonts: FontSystem,
|
||||
swash: SwashCache,
|
||||
buffers: HashMap<TextKey, PreparedText>,
|
||||
source_bytes: usize,
|
||||
}
|
||||
|
||||
impl TextEngine {
|
||||
fn new() -> Self {
|
||||
let mut fonts = FontSystem::new();
|
||||
// Select installed, readable families rather than assuming an OS font
|
||||
// name exists. COSMIC Text handles per-glyph fallback (including CJK).
|
||||
let choose = |choices: &[&str]| {
|
||||
choices.iter().find_map(|choice| {
|
||||
fonts
|
||||
.db()
|
||||
.faces()
|
||||
.any(|face| face.families.iter().any(|(name, _)| name == choice))
|
||||
.then(|| (*choice).to_owned())
|
||||
})
|
||||
};
|
||||
let sans = choose(&[
|
||||
".AppleSystemUIFont",
|
||||
"SF Pro Text",
|
||||
"Helvetica Neue",
|
||||
"Segoe UI",
|
||||
"Noto Sans",
|
||||
"DejaVu Sans",
|
||||
"Liberation Sans",
|
||||
]);
|
||||
let mono = choose(&["Menlo", "Consolas", "DejaVu Sans Mono", "Liberation Mono"]);
|
||||
if let Some(name) = sans {
|
||||
fonts.db_mut().set_sans_serif_family(name);
|
||||
}
|
||||
if let Some(name) = mono {
|
||||
fonts.db_mut().set_monospace_family(name);
|
||||
}
|
||||
Self {
|
||||
fonts,
|
||||
swash: SwashCache::new(),
|
||||
buffers: HashMap::new(),
|
||||
source_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare(&mut self, value: &str, style: TextStyle) -> TextKey {
|
||||
let key = TextKey {
|
||||
value: value.to_owned(),
|
||||
font_size: style.font_size.to_bits(),
|
||||
line_height: style.line_height.to_bits(),
|
||||
family: style.family,
|
||||
};
|
||||
if self.buffers.contains_key(&key) {
|
||||
return key;
|
||||
}
|
||||
// Cache recent shaped strings, without retaining every submitted source.
|
||||
if self.buffers.len() >= 256 || self.source_bytes + value.len() > 524_288 {
|
||||
self.buffers.clear();
|
||||
self.source_bytes = 0;
|
||||
}
|
||||
let family = match style.family {
|
||||
TextFamily::Interface => Family::SansSerif,
|
||||
TextFamily::Code => Family::Monospace,
|
||||
};
|
||||
let mut buffer = Buffer::new(
|
||||
&mut self.fonts,
|
||||
Metrics::new(style.font_size, style.line_height),
|
||||
);
|
||||
buffer.set_wrap(Wrap::None);
|
||||
buffer.set_size(None, None);
|
||||
buffer.set_text(
|
||||
value,
|
||||
&Attrs::new().family(family),
|
||||
Shaping::Advanced,
|
||||
Some(Align::Left),
|
||||
);
|
||||
buffer.shape_until_scroll(&mut self.fonts, false);
|
||||
let mut size = TextSize {
|
||||
width: 0.0,
|
||||
height: style.line_height,
|
||||
};
|
||||
for run in buffer.layout_runs() {
|
||||
size.width = size.width.max(run.line_w);
|
||||
size.height = size.height.max(run.line_top + run.line_height);
|
||||
}
|
||||
self.source_bytes += value.len();
|
||||
self.buffers
|
||||
.insert(key.clone(), PreparedText { buffer, size });
|
||||
key
|
||||
}
|
||||
}
|
||||
|
||||
fn engine() -> &'static Mutex<TextEngine> {
|
||||
static ENGINE: OnceLock<Mutex<TextEngine>> = OnceLock::new();
|
||||
ENGINE.get_or_init(|| Mutex::new(TextEngine::new()))
|
||||
}
|
||||
|
||||
pub fn measure(value: &str, style: TextStyle) -> TextSize {
|
||||
let mut engine = engine().lock().expect("text engine lock");
|
||||
let key = engine.prepare(value, style);
|
||||
engine.buffers[&key].size
|
||||
}
|
||||
|
||||
/// Draw at the actual device font size; no scaling of an already rasterized bitmap.
|
||||
pub fn draw(value: &str, style: TextStyle, color: Color, mut pixel: impl FnMut(i32, i32, Color)) {
|
||||
let mut engine = engine().lock().expect("text engine lock");
|
||||
let key = engine.prepare(value, style);
|
||||
let TextEngine {
|
||||
fonts,
|
||||
swash,
|
||||
buffers,
|
||||
..
|
||||
} = &mut *engine;
|
||||
let opacity = u16::from(color.alpha);
|
||||
buffers.get_mut(&key).expect("prepared text").buffer.draw(
|
||||
fonts,
|
||||
swash,
|
||||
cosmic_text::Color::rgb(color.red, color.green, color.blue),
|
||||
|x, y, width, height, color| {
|
||||
for dy in 0..height as i32 {
|
||||
for dx in 0..width as i32 {
|
||||
pixel(
|
||||
x + dx,
|
||||
y + dy,
|
||||
Color::rgba(
|
||||
color.r(),
|
||||
color.g(),
|
||||
color.b(),
|
||||
(u16::from(color.a()) * opacity / 255) as u8,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
// The cache is an optimization, never an archive of all submitted glyphs.
|
||||
if swash.image_cache.len() > 4096
|
||||
|| swash
|
||||
.image_cache
|
||||
.values()
|
||||
.flatten()
|
||||
.map(|image| image.data.len())
|
||||
.sum::<usize>()
|
||||
> 33_554_432
|
||||
{
|
||||
*swash = SwashCache::new();
|
||||
}
|
||||
}
|
||||
|
||||
/// Fit a single displayed source line without slicing a UTF-8/grapheme cluster.
|
||||
pub fn fit(value: &str, width: f32, style: TextStyle) -> String {
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
// A source line may include a very large comment. Only shape a bounded
|
||||
// preview for the sidebar; the stored source and its AST spans stay intact.
|
||||
let truncated = value.len() > 8192;
|
||||
let value = if truncated {
|
||||
let end = value
|
||||
.grapheme_indices(true)
|
||||
.map(|(index, _)| index)
|
||||
.take_while(|index| *index <= 8192)
|
||||
.last()
|
||||
.unwrap_or(0);
|
||||
&value[..end]
|
||||
} else {
|
||||
value
|
||||
};
|
||||
if !truncated && measure(value, style).width <= width {
|
||||
return value.to_owned();
|
||||
}
|
||||
let ellipsis = "…";
|
||||
if width <= 0.0 || measure(ellipsis, style).width > width {
|
||||
return String::new();
|
||||
}
|
||||
let ends: Vec<_> = value
|
||||
.grapheme_indices(true)
|
||||
.map(|(start, part)| start + part.len())
|
||||
.collect();
|
||||
let mut low = 0;
|
||||
let mut high = ends.len();
|
||||
while low < high {
|
||||
let count = (low + high).div_ceil(2);
|
||||
let end = ends[count - 1];
|
||||
let candidate = format!("{}{ellipsis}", &value[..end]);
|
||||
if measure(&candidate, style).width <= width {
|
||||
low = count;
|
||||
} else {
|
||||
high = count - 1;
|
||||
}
|
||||
}
|
||||
let end = low.checked_sub(1).map_or(0, |index| ends[index]);
|
||||
format!("{}{ellipsis}", &value[..end])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[test]
|
||||
fn proportional_metrics_monospace_and_multiline_height_come_from_the_fonts() {
|
||||
let ui = TextStyle::ui(1);
|
||||
assert!(measure("WWWW", ui).width > measure("iiii", ui).width * 2.0);
|
||||
let mono = TextStyle::code();
|
||||
assert!((measure("WWWW", mono).width - measure("iiii", mono).width).abs() < 0.1);
|
||||
assert_eq!(measure("one\ntwo", ui).height, ui.line_height * 2.0);
|
||||
let width = measure("Hello Endor", ui).width;
|
||||
assert!((measure("Hello Endor", ui.scaled(1.25)).width - width * 1.25).abs() < 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_fallback_shapes_chinese_without_missing_glyphs() {
|
||||
let mut engine = engine().lock().unwrap();
|
||||
for style in [TextStyle::ui(1), TextStyle::code()] {
|
||||
let key = engine.prepare("中文字体", style);
|
||||
let glyphs: Vec<_> = engine.buffers[&key]
|
||||
.buffer
|
||||
.layout_runs()
|
||||
.flat_map(|run| run.glyphs)
|
||||
.collect();
|
||||
assert_eq!(glyphs.len(), 4);
|
||||
assert!(glyphs.iter().all(|glyph| glyph.glyph_id != 0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ellipsis_fits_actual_width_and_preserves_graphemes() {
|
||||
let style = TextStyle::code();
|
||||
let value = "e\u{301}e\u{301}中文字体e\u{301}e\u{301}";
|
||||
let width = measure("e\u{301}e\u{301}中…", style).width + 0.01;
|
||||
let fitted = fit(value, width, style);
|
||||
let prefix = fitted.strip_suffix('…').unwrap();
|
||||
assert!(value.starts_with(prefix));
|
||||
assert!(
|
||||
value
|
||||
.grapheme_indices(true)
|
||||
.any(|(index, _)| index == prefix.len())
|
||||
);
|
||||
assert!(measure(&fitted, style).width <= width);
|
||||
assert_eq!(fit(value, 0.0, style), "");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user