mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 15:49:54 +00:00
Merge branch 'lang-2-ui' into lang-3-fonts
This commit is contained in:
commit
402828151d
15 files changed
+90
-28
No files matched your search
@@ -3,9 +3,9 @@ name: Shared UI
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ["crates/ui/**", "crates/i18n/**", "Cargo.toml", "Cargo.lock", ".github/workflows/ui.yml"]
|
||||
paths: ["crates/ui/**", "crates/i18n/**", "crates/fonts-build/**", "Cargo.toml", "Cargo.lock", ".github/workflows/ui.yml"]
|
||||
pull_request:
|
||||
paths: ["crates/ui/**", "crates/i18n/**", "Cargo.toml", "Cargo.lock", ".github/workflows/ui.yml"]
|
||||
paths: ["crates/ui/**", "crates/i18n/**", "crates/fonts-build/**", "Cargo.toml", "Cargo.lock", ".github/workflows/ui.yml"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ author, source and license. Library code and original assets are under the repos
|
||||
| `crates/i18n/data/collation.postcard` | Unicode, Inc. (ICU 78.1 collation data, CLDR 48.2.1); generated by `crates/i18n/data/generate-collation.sh` | [ICU](https://github.com/unicode-org/icu/releases/tag/release-78.1rc) | Unicode-3.0 |
|
||||
| `crates/text/assets/hyphen/en-us.dic` | Grady Ward (Moby Hyphenator II), converted by DesignCraft | [Project Gutenberg eBook #3204](https://www.gutenberg.org/ebooks/3204) | Public domain |
|
||||
| `crates/text/assets/hyphen/en-us.pat` | DesignCraft contributors, trained from the Moby Hyphenator II list | `crates/text/examples/hyphgen.rs` | CC0-1.0 |
|
||||
| Patterns embedded through `hypher` (optional `craft-text` language features) | hyph-utf8 authors, per language | [tex-hyphen](https://github.com/hyphenation/tex-hyphen), as packaged in [hypher](https://github.com/typst/hypher/tree/main/patterns) | Per file header: LPPL, MIT, BSD-3-Clause, Unlicense or a permit-any-use notice (or a choice including one); Slovak (`hyph-sk.tex`, feature `slovak-gpl` only) is GPL-2.0-or-later; Spanish (`hyph-es.tex` 5.0, Javier Bezos, CervanTeX) is MIT |
|
||||
| Patterns embedded through `hypher` (optional `craft-text` language features) | hyph-utf8 authors, per language | [tex-hyphen](https://github.com/hyphenation/tex-hyphen), as packaged in [hypher](https://github.com/typst/hypher/tree/main/patterns) | Per file header: LPPL, MIT, BSD-3-Clause, Unlicense or a permit-any-use notice (or a choice including one); Slovak (`hyph-sk.tex`, embedded whenever `hypher/slovak` is on: feature `slovak-gpl`, or another dependency through feature unification) is GPL-2.0-or-later; Spanish (`hyph-es.tex` 5.0, Javier Bezos, CervanTeX) is MIT |
|
||||
| Word-break dictionaries and LSTM models embedded through `icu_segmenter_data` (optional `craft-text` features `thai`, `lao`, `khmer`, `burmese`) | Unicode, Inc. and ICU4X contributors | [ICU4X](https://github.com/unicode-org/icu4x) | Unicode-3.0 |
|
||||
| `crates/ui/tests/fonts/NotoSansArabic-test.ttf` (subset to the test strings, default weight set to 100, width axis dropped) | The Noto Project Authors | [google/fonts@7085eb8 `ofl/notosansarabic`](https://github.com/google/fonts/tree/7085eb89a950e85db5b166b7a58d414544b4140c/ofl/notosansarabic), via craft-fonts | OFL-1.1 (`licenses/ui-test-fonts/OFL-NotoSansArabic.txt`) |
|
||||
| `crates/ui/tests/fonts/NotoSansCJKsc-test.otf` (subset to the test strings) | The Noto Project Authors (Adobe, Google) | [notofonts/noto-cjk@f8d1575 `Sans/OTF/SimplifiedChinese`](https://github.com/notofonts/noto-cjk/tree/f8d157532fbfaeda587e826d4cd5b21a49186f7c/Sans/OTF/SimplifiedChinese), via craft-fonts | OFL-1.1 (`licenses/ui-test-fonts/OFL-NotoSansCJK.txt`) |
|
||||
|
||||
@@ -29,9 +29,11 @@ Hyphenation patterns for other languages, embedded through the hypher crate only
|
||||
Unlicense, a notice permitting any use, or a choice of licenses that includes one of these. An
|
||||
application that enables a language ships that file's copyright and license notice. The
|
||||
exception is Slovak (hyph-sk.tex, Copyright (C) 1992 Jana Chlebíková), under the GNU General
|
||||
Public License, version 2 or later (SPDX: GPL-2.0-or-later). It is compiled in only with the
|
||||
Public License, version 2 or later (SPDX: GPL-2.0-or-later). craft-text asks for it through the
|
||||
feature "slovak-gpl", which "all-languages" does not include; an application that enables it
|
||||
ships GPL-licensed data. The Spanish patterns (feature "spanish") are hyph-es.tex 5.0.
|
||||
ships GPL-licensed data. Cargo unifies features, so another dependency that enables hypher's
|
||||
"slovak" feature (hypher's default features do) embeds the same data; a build that must exclude
|
||||
it checks its dependency graph. The Spanish patterns (feature "spanish") are hyph-es.tex 5.0.
|
||||
Copyright (C) 1993, 1997 Javier Bezos, 2001-2019 Javier Bezos, CervanTeX. MIT License:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
and associated documentation files (the "Software"), to deal in the Software without
|
||||
|
||||
@@ -138,7 +138,7 @@ it has: about 3 KB for a CJK interface face cut to an app's catalogs, under 1 KB
|
||||
An interface face cut by `subset_to` is served from its cut in `OUT_DIR`, with the start of the
|
||||
cut's SHA-256 in its name (`web/fonts/ui/NotoSansTCUI-Regular.1234abcd5678ef90.ttf`) and the cut's
|
||||
own `integrity`, so changed catalogs give a new URL rather than a cached copy that fails its check.
|
||||
A relative `dir` is under the package directory. Files already there with the right size are not
|
||||
A relative `dir` is under the package directory. Files already there with the same bytes are not
|
||||
copied again, a deleted one is copied again on the next build, and files of faces no longer
|
||||
selected, and earlier cuts, stay.
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ impl Embed {
|
||||
/// `web/fonts` as `fonts/`. An interface face cut by `Embed::subset_to` is served from its cut,
|
||||
/// with the start of the cut's SHA-256 in the file name (`ui/NotoSansTCUI-Regular.<hash>.ttf`)
|
||||
/// and the cut's own integrity value. A relative `dir` is under the package directory, where
|
||||
/// Cargo runs build scripts. Files already there with the right size are left as they are;
|
||||
/// Cargo runs build scripts. Files already there with the same bytes are left as they are;
|
||||
/// files of faces no longer selected, and earlier cuts, are not removed.
|
||||
pub fn serve(mut self, dir: impl Into<PathBuf>, url_prefix: &str) -> Self {
|
||||
self.serve = Some(Serve { dir: dir.into(), url_prefix: url_prefix.to_owned() });
|
||||
@@ -555,21 +555,22 @@ fn served_path(file: &str) -> &str {
|
||||
file.strip_prefix("fonts/").unwrap_or(file)
|
||||
}
|
||||
|
||||
/// Copy `from` unless `to` already has its size. The copy keeps `from`'s modification time and is
|
||||
/// watched, so the build copies it again once it is deleted, without running every build.
|
||||
/// Copy `from` unless `to` already holds the same bytes. The copy keeps `from`'s modification time
|
||||
/// and is watched, so the build copies it again once it is deleted, without running every build.
|
||||
fn copy_file(from: &Path, to: &Path, cargo: &mut Vec<String>) -> Result<(), Error> {
|
||||
cargo.push(format!("cargo::rerun-if-changed={}", to.display()));
|
||||
let write_error = |source| Error::Write { path: to.to_path_buf(), source };
|
||||
let size = |path: &Path| std::fs::metadata(path).ok().map(|m| m.len());
|
||||
if size(to).is_some() && size(to) == size(from) {
|
||||
let read_error = |source| Error::Read { path: from.to_path_buf(), source };
|
||||
let bytes = std::fs::read(from).map_err(read_error)?;
|
||||
if std::fs::read(to).is_ok_and(|old| old == bytes) {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(parent) = to.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(write_error)?;
|
||||
}
|
||||
std::fs::copy(from, to).map_err(write_error)?;
|
||||
std::fs::write(to, &bytes).map_err(write_error)?;
|
||||
// Cargo reruns a build script whose watched files are newer than its last run.
|
||||
let modified = std::fs::metadata(from).and_then(|m| m.modified()).map_err(|source| Error::Read { path: from.to_path_buf(), source })?;
|
||||
let modified = std::fs::metadata(from).and_then(|m| m.modified()).map_err(read_error)?;
|
||||
std::fs::File::options().write(true).open(to).and_then(|f| f.set_modified(modified)).map_err(write_error)
|
||||
}
|
||||
|
||||
|
||||
@@ -232,6 +232,18 @@ fn serve_copies_the_selected_faces_and_writes_urls() {
|
||||
assert!(!empty.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_replaces_a_stale_copy_of_the_same_size() {
|
||||
let scratch = Scratch::new("serve-stale");
|
||||
let served = scratch.0.join("served");
|
||||
let source = std::fs::read(fixture().join("fonts/sans-jp/SansJP-Regular.ttf")).unwrap();
|
||||
std::fs::create_dir_all(served.join("sans-jp")).unwrap();
|
||||
std::fs::write(served.join("sans-jp/SansJP-Regular.ttf"), vec![0; source.len()]).unwrap();
|
||||
let r = run(Embed::new().scripts(&["Jpan"]).filter(|f| f.style == "Regular").serve(&served, "fonts/"), Some(&fixture()), Some("1"));
|
||||
r.result.unwrap();
|
||||
assert_eq!(std::fs::read(served.join("sans-jp/SansJP-Regular.ttf")).unwrap(), source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integrity_is_base64_of_the_hex_hash() {
|
||||
use crate::embed::integrity;
|
||||
|
||||
@@ -138,7 +138,9 @@ russian = ["hyphen", "hypher/russian"]
|
||||
sanskrit = ["hyphen", "hypher/sanskrit"]
|
||||
serbian = ["hyphen", "hypher/serbian"]
|
||||
# Slovak patterns are under the GPL, version 2 or later (hyph-sk.tex), unlike every other language
|
||||
# here, so `all-languages` leaves them out. A build that enables this ships GPL-licensed data.
|
||||
# here, so `all-languages` leaves them out. A build that enables this ships GPL-licensed data. Any
|
||||
# other dependency that enables `hypher/slovak` (hypher's defaults do) embeds them too, through
|
||||
# feature unification.
|
||||
slovak-gpl = ["hyphen", "hypher/slovak"]
|
||||
slovenian = ["hyphen", "hypher/slovenian"]
|
||||
spanish = ["hyphen", "hypher/spanish"]
|
||||
|
||||
@@ -42,8 +42,11 @@ Each has a cargo feature named as in hypher (`spanish`, `german`, `norwegian`,
|
||||
enables 46 of them. Patterns for languages whose feature is off are not compiled in.
|
||||
|
||||
Slovak's patterns are under the GPL, version 2 or later; the patterns of every other language are
|
||||
under a permissive license. Their feature is `slovak-gpl`, and `all-languages` leaves it out: a build that
|
||||
enables it ships GPL-licensed data.
|
||||
under a permissive license. craft-text asks for Slovak through the feature `slovak-gpl`, which
|
||||
`all-languages` leaves out; a build that enables it ships GPL-licensed data. Cargo unifies features,
|
||||
so another dependency that enables hypher's `slovak` feature (hypher's default features do) embeds
|
||||
the same data. A build that must exclude it checks its dependency graph
|
||||
(`cargo tree -e features -i hypher`).
|
||||
|
||||
```rust
|
||||
use craft_text::hyphen::{Lang, Limits, hyphen_points_in};
|
||||
|
||||
@@ -161,16 +161,30 @@ impl Lang {
|
||||
}
|
||||
|
||||
/// The language of a locale code split into lowercase `words`: a language (2 or 3 letters), then
|
||||
/// optionally a region (2 letters or 3 digits) and a reform year (4 digits).
|
||||
/// optionally a script (4 letters), a region (2 letters or 3 digits) and a reform year (4 digits),
|
||||
/// in that order.
|
||||
fn locale_language<'a>(words: &[&'a str]) -> Option<&'a str> {
|
||||
let letters = |w: &str, n: std::ops::RangeInclusive<usize>| n.contains(&w.len()) && w.bytes().all(|b| b.is_ascii_lowercase());
|
||||
let digits = |w: &str, n: usize| w.len() == n && w.bytes().all(|b| b.is_ascii_digit());
|
||||
match *words {
|
||||
[lang] if letters(lang, 2..=3) => Some(lang),
|
||||
[lang, region] if letters(lang, 2..=3) && (letters(region, 2..=2) || digits(region, 3)) => Some(lang),
|
||||
[lang, region, year] if letters(lang, 2..=3) && (letters(region, 2..=2) || digits(region, 3)) && digits(year, 4) => Some(lang),
|
||||
_ => None,
|
||||
fn letters(w: &str, n: std::ops::RangeInclusive<usize>) -> bool {
|
||||
n.contains(&w.len()) && w.bytes().all(|b| b.is_ascii_lowercase())
|
||||
}
|
||||
fn digits(w: &str, n: usize) -> bool {
|
||||
w.len() == n && w.bytes().all(|b| b.is_ascii_digit())
|
||||
}
|
||||
let (&lang, mut rest) = words.split_first()?;
|
||||
if !letters(lang, 2..=3) {
|
||||
return None;
|
||||
}
|
||||
let script = |w: &str| letters(w, 4..=4);
|
||||
let region = |w: &str| letters(w, 2..=2) || digits(w, 3);
|
||||
let year = |w: &str| digits(w, 4);
|
||||
for subtag in [script as fn(&str) -> bool, region, year] {
|
||||
if let Some((&w, tail)) = rest.split_first()
|
||||
&& subtag(w)
|
||||
{
|
||||
rest = tail;
|
||||
}
|
||||
}
|
||||
rest.is_empty().then_some(lang)
|
||||
}
|
||||
|
||||
/// Where a word's break points came from.
|
||||
|
||||
@@ -70,7 +70,7 @@ fn limits_are_respected() {
|
||||
|
||||
#[test]
|
||||
fn language_names_and_codes() {
|
||||
for name in ["English", "English: USA", "english", "$ID/English: UK", "en", "en_US", "en-GB", "EN"] {
|
||||
for name in ["English", "English: USA", "english", "$ID/English: UK", "en", "en_US", "en-GB", "EN", "en-Latn-US"] {
|
||||
assert_eq!(Lang::for_language(name), Some(Lang::English), "{name}");
|
||||
}
|
||||
// Each language resolves by name and by code exactly when its feature is on.
|
||||
@@ -88,6 +88,10 @@ fn language_names_and_codes() {
|
||||
format!("{code}_{upper}"),
|
||||
format!("{code}-419"),
|
||||
format!("{code}_{upper}_2006"),
|
||||
format!("{code}-Latn"),
|
||||
format!("{code}-Latn-{upper}"),
|
||||
format!("{code}-1996"),
|
||||
format!("{code}-Latn-1996"),
|
||||
] {
|
||||
assert_eq!(Lang::for_language(&spelling), want, "{spelling}");
|
||||
}
|
||||
|
||||
@@ -77,6 +77,13 @@ impl From<craft_fonts_build::EmbeddedFace> for CraftFace {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<craft_fonts_build::ServedFace> for ServedFace {
|
||||
fn from(face: craft_fonts_build::ServedFace) -> Self {
|
||||
let craft_fonts_build::ServedFace { family, style, scripts, index, url, size, integrity, coverage } = face;
|
||||
Self { family, style, scripts, index, url, size, integrity, coverage }
|
||||
}
|
||||
}
|
||||
|
||||
impl CraftFace {
|
||||
/// The egui font name, e.g. `"Noto Sans Arabic Regular"`.
|
||||
pub fn name(&self) -> String {
|
||||
|
||||
@@ -581,11 +581,13 @@ fn is_shortcut_chord(m: Modifiers) -> bool {
|
||||
/// A caret IMEs can place a window at: at least a point wide and tall (some ignore an empty
|
||||
/// rectangle), and inside the window, so a caret scrolled off the canvas doesn't put the
|
||||
/// candidates off screen.
|
||||
fn caret_area(caret: Rect, screen: Rect) -> Rect {
|
||||
pub(super) fn caret_area(caret: Rect, screen: Rect) -> Rect {
|
||||
if !caret.is_finite() || !screen.is_positive() {
|
||||
return caret;
|
||||
}
|
||||
let size = caret.size().max(egui::vec2(1.0, 1.0)).min(screen.size());
|
||||
let min = caret.min.clamp(screen.min, screen.max - size);
|
||||
// `screen.max - size` can round below `screen.min` (a screen from 0.1 to 0.5), and `clamp`
|
||||
// panics on bounds out of order.
|
||||
let min = caret.min.clamp(screen.min, (screen.max - size).max(screen.min));
|
||||
Rect::from_min_size(min, size)
|
||||
}
|
||||
@@ -538,3 +538,13 @@ fn a_caret_off_the_canvas_keeps_the_candidates_on_screen() {
|
||||
let r = out.platform_output.ime.map(|i| i.cursor_rect);
|
||||
assert_eq!(r, Some(Rect::from_min_size(pos2(0.0, 580.0), vec2(2.0, 20.0))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_caret_past_a_tiny_or_unbounded_screen_stays_inside_it() {
|
||||
let caret = Rect::from_min_size(pos2(10.0, 10.0), vec2(2.0, 20.0));
|
||||
let tiny = Rect::from_min_max(pos2(0.1, 0.1), pos2(0.5, 0.5));
|
||||
assert_eq!(composer::caret_area(caret, tiny), Rect::from_min_size(tiny.min, tiny.size()));
|
||||
assert_eq!(composer::caret_area(caret, Rect::EVERYTHING), caret);
|
||||
let nan = Rect::from_min_max(pos2(0.0, 0.0), pos2(f32::NAN, 600.0));
|
||||
assert_eq!(composer::caret_area(caret, nan), caret);
|
||||
}
|
||||
@@ -127,7 +127,10 @@ impl Screens {
|
||||
}
|
||||
|
||||
/// Called on the context before the first frame, after the fonts are installed: the app's
|
||||
/// theme, and its interface direction if it stores one on the context.
|
||||
/// theme, and anything else it stores on the context. The harness applies the locale's
|
||||
/// direction ([`direction::apply`](crate::direction::apply)) before this runs and again after
|
||||
/// it, so egui's own chrome follows the locale even when the callback installs a whole new
|
||||
/// style. The callback can't override the direction.
|
||||
pub fn setup(mut self, setup: impl Fn(&Context, &Locale) + 'static) -> Self {
|
||||
self.setup = Some(Rc::new(setup));
|
||||
self
|
||||
@@ -153,8 +156,10 @@ impl Screens {
|
||||
.with_size(self.size)
|
||||
.with_context_setup(move |ctx| {
|
||||
fonts.install(ctx);
|
||||
crate::direction::apply(ctx, locale.direction);
|
||||
if let Some(setup) = setup {
|
||||
setup(ctx, &locale);
|
||||
crate::direction::apply(ctx, locale.direction);
|
||||
}
|
||||
})
|
||||
.build_ui_state(
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Reference in new issue
Block a user