feat: bundle Luciole, Literata, Charis SIL and a Hebrew fallback face

This commit is contained in:
randogoth 2026-10-11 14:24:06 +03:00
parent dfba1cfb16
commit 6de6672ae3
30 changed files with 624 additions and 11 deletions

View file

@ -73,4 +73,6 @@ The v1 client presents one reader screen:
- Each library page carries a search box that narrows it by title or address, and each row carries its own actions: remove a bookmark, leave a site or check it now, forget a page. History lists one row per address, headed by the most recent visit, rather than repeating a page once per visit. Clearing history takes a second step on a page the reader can simply leave, since it cannot be undone.
- A change is a link too: following `mews:do/<verb>` applies it and rerenders the page in place without adding a history entry. Only a link followed from one of the client's own pages is honored, so a fetched page cannot clear history or change a setting on the reader's behalf (spec 8.1). A form on one of these pages is routed the same way rather than sent to the network.
- Our own pages are built from the same blocks as a page but are interface, not an article, so the renderer lays them out differently: wider before they stop growing, denser, rows divided by a line instead of bulleted, section headings quieter, a note under a row set smaller and dimmer, links carried by weight rather than an underline, and a one-field form on a single line.
- Fonts are bundled, and v1 ships Atkinson Hyperlegible. The reader's typeface applies to the whole app, chrome included, so the address bar and the page below it are one face. Adding a font is one TTF in `app/assets/fonts/`, one `fonts` entry in `app/pubspec.yaml`, one option on the settings page, and one label in `app/locales/en-US/main.ftl`.
- Fonts are bundled, never fetched. The reader picks between two sans faces, Atkinson Hyperlegible and Luciole, and two serifs, Literata and Charis SIL; all four carry regular, italic, bold and bold italic, which the renderer needs for emphasis. The chosen face applies to the whole app, chrome included, so the address bar and the page below it are one face.
- A character the chosen face has no glyph for is looked for in that face's own fallbacks before the platform is asked. Hebrew always comes from a bundled Hebrew face matched to the shape of the chosen one, so it never depends on what the device has installed. The serifs also cover Greek, Cyrillic and a wide Latin range and fall back to each other; the two sans faces are narrow and have no bundled sans to turn to, so for those the platform answers, and it answers a sans with a sans. Scripts nothing bundled covers, such as Arabic and the CJK languages, always come from the platform.
- Adding a font is four TTFs in `app/assets/fonts/` with its licence beside them, one `fonts` entry in `app/pubspec.yaml`, one entry in `app/lib/application/fonts.dart`, and one label in `app/locales/en-US/main.ftl`. A family named in the registry but missing from pubspec would render in the platform's font without saying so, so `app/test/fonts_test.dart` checks that the two agree.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,94 @@
Copyright (c) 1997-2025 SIL Global (https://www.sil.org/)
with Reserved Font Names "Charis" and "SIL".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,93 @@
Copyright 2017 The Literata Project Authors (https://github.com/googlefonts/literata)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,126 @@
Luciole package
===============
## Description
This package provides four Unicode Math text fonts `Luciole-*.ttf` and
a companion maths font `Luciole-Math.otf`.
They have been developed explicitly for visually impaired people
and are the result of a collaboration between the Regional Technical Center
for Visual Impairment, the type-design studio typographies.fr and
the mathematician Daniel Flipo. The Luciole project received a grant from
the Swiss Ceres Foundation and the PEP69 association, and support from
the DIPHE laboratory at the Université Lumière Lyon 2 and the association
GUTenberg.
## Contents
* Luciole-Regular.ttf, Luciole-Regular-Italic.ttf, Luciole-Bold.ttf and
Luciole-BoldItalic.ttf TrueType Text fonts
* Luciole-Math.otf OpenType Math font
* Luciole-Math-Bold.otf Bold variant (limited coverage)
* luciole-math.sty LaTeX style file for Luciole-Math.otf
* Luciole.fontspec Defaults for Text fonts
* Luciole-Doc.pdf Documentation in PDF format
* Luciole-Doc.ltx Source of Luciole-Math.pdf
* unimath-luciole.pdf Modified version of unimath-symbols.pdf
showing available Luciole-Math symbols compared
to other Maths fonts,
* unimath-luciole.ltx Source of unimath-luciole.pdf
* README.md (this file)
## Installation
This package `Luciole’ is meant to be installed automatically by TeXLive,
MikTeX, etc. Otherwise, can be installed under TEXMFHOME or TEXMFLOCAL, f.i.
text and math fonts in directory texmf-local/fonts/truetype/public/luciole/,
and luciole-math.sty, Luciole.fontspec in directory
texmf-local/tex/latex/luciole/.
Documentation files and their sources can go to directory
texmf-local/doc/fonts/public/luciole/
Don't forget to rebuild the file database (mktexlsr or so) if you install
under TEXMFLOCAL.
Finally, make the system font database aware of the Luciole fonts
(fontconfig under Linux).
## License
* The Text fonts `Luciole-*.ttf` are licensed under the Creative Common CC-BY
Font License,
* The font `Luciole-Math.otf` is licensed under the SIL Open Font License,
Version 1.1. This license is available with a FAQ at:
http://scripts.sil.org/OFL
* The other files are distributed under the terms of the LaTeX Project
Public License from CTAN archives in directory macros/latex/base/lppl.txt.
Either version 1.3c or, at your option, any later version.
## Changes
* First public version: 0.40
* v0.50
- internal: \widearc reencoded for compatibility with my other math fonts.
- cv00 (unavailable) replaced by cv11 to access slashed zeros.
- `luciole-math.sty` tunes \TeX, \LaTeX, \LaTeXe logos for the Luciole fonts;
option "no-logos" added to prohibit their redefinition.
* v0.55
- Internal: \std@equal redefined, redefinition of \std@minus removed.
- Most horizontal arrows and harpoons are now stretchable.
- Fixed kernings between ([{ and \mij, \mity.
- Math table fixed for glyphs horizontal and vertical composition.
* v0.60
- Improved design of large parentheses.
- Invisible Math operators (U-2061 to U+2064) added.
- Compatibility with Typst v0.14: all glyphs used in vertical compositions
now sit on the baseline.
- Documentation fixes.
* v0.70
- Script variant added for calligraphic capitals.
- Circle, diamond and lozenge operators : sizes enlarged.
- Brackets redesigned.
- Math table Sub/Superscripts parameters tuned, see
https://tex.stackexchange.com/questions/759695/
- Math table fraction parameters tuned.
- Fixed spaces' width (U+2000 to U+200B and U+205F).
- Variation Selectors (U-FE00 and U+FE01) added; they work on all Math
symbols listed on
https://www.unicode.org/Public/UCD/latest/ucd/StandardizedVariants.txt
which are available in Luciole Math fonts (U+FE01 provides access to
script variants of the default calligraphic capitals (Chancery like).
- Documentation fixes.
* v0.71
- Superscripts for calligraphic capitals redesigned.
* v0.75
- Text fonts updated to version 2.001.
- \setminus (U+2113) and \smallsetminus (U+E074) have now different glyphs.
- \ell (U+2113) is now upright. \mscrl (U+1D4C1) and \mbfscrl (U+1D4F5)
added; they are slanted versions of \ell and \mbfell (U+E055).
- New CharacterVariants cv12 and cv13 provide alternative shapes
for the lowercase l which might be confused with uppercase I (thanks
to Baptiste Auguié for suggesting this).
- New CharacterVariant cv14 provides an alternative shape for 'a'.
- Glyphs \mscr (U+1D4C7) and \mbfscr (U+1D4FB) added for Griffith's
curly 'r'.
- Over eighty Math symbols added to Luciole-Math-Bold.
* v0.76
- Lowercase \gamma (regular, bold, italic, bolditalic) redesigned
to avoid confusion with y (suggested by Baptiste Auguié).
---
Copyright 2024-2026 Laurent Bourcellier, Jonathan Fabreguettes, Daniel Flipo
E-mail: daniel (dot) flipo (at) free (dot) fr
## Attribution for this bundle
Luciole by Laurent Bourcellier and Jonathan Perez (typographies.fr) with the
Centre Technique Régional pour la Déficience Visuelle, licensed CC BY 4.0:
https://creativecommons.org/licenses/by/4.0/
Project home: https://luciole-vision.com/

Binary file not shown.

View file

@ -0,0 +1,93 @@
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/hebrew)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,6 +1,7 @@
import 'package:signals/signals.dart';
import '../../database/database.dart';
import '../fonts.dart';
/// The reader's presentation choices (spec 8.2) plus the per-app limits they
/// move within. Values persist through ReaderSettings in the database.
@ -19,7 +20,7 @@ class ReaderSettingsController {
static const themeModes = ['light', 'dark', 'system'];
static const variants = ['site', 'warm', 'cool', 'green', 'mono'];
final font = signal('Atkinson Hyperlegible');
final font = signal(defaultFontFamily);
final fontScale = signal(1.0);
final themeMode = signal('system');
final variant = signal('site');
@ -34,7 +35,11 @@ class ReaderSettingsController {
}
Future<bool> setFont(String family) async {
if (family.isEmpty || family == font.value) return false;
// Only a family this build bundles: an unknown one would silently render
// in the platform's font instead.
if (!readerFonts.any((f) => f.family == family) || family == font.value) {
return false;
}
font.value = family;
await _persist();
return true;

View file

@ -0,0 +1,60 @@
/// The typefaces bundled with the client (spec 8.2: the reader chooses).
/// Pure data, shared by the settings page that offers them and the renderer
/// that applies them.
library;
/// One bundled family: the name used in pubspec.yaml and stored in settings,
/// the id of its display name in the locale file, and where to look when it
/// has no glyph for a character.
typedef ReaderFont = ({String family, String labelId, List<String> fallback});
/// Offered in this order: the two sans faces first, then the two serifs.
/// Adding a font is four files in assets/fonts, one `fonts` entry in
/// pubspec.yaml, one entry here, one label in the locale file, and its licence
/// beside the files.
///
/// Each face names where to look for a character it has no glyph for. Hebrew
/// comes from a bundled Hebrew face, matched to the shape of the chosen one,
/// so it never depends on what the device happens to have installed. The
/// serifs also cover Greek, Cyrillic and a wide Latin range, so each falls
/// back to the other; the sans faces are narrow and have no bundled sans to
/// turn to, so for those the platform answers, and it answers a sans with a
/// sans. Scripts nothing here covers, such as Arabic and the CJK languages,
/// always come from the platform.
const readerFonts = <ReaderFont>[
(
family: 'Atkinson Hyperlegible',
labelId: 'settings-font-atkinson',
fallback: [hebrewSans],
),
(
family: 'Luciole',
labelId: 'settings-font-luciole',
fallback: [hebrewSans],
),
(
family: 'Literata',
labelId: 'settings-font-literata',
fallback: ['Charis SIL', hebrewSerif],
),
(
family: 'Charis SIL',
labelId: 'settings-font-charis',
fallback: ['Literata', hebrewSerif],
),
];
/// Bundled to be fallen back on, never offered: the reader picks a face for
/// the Latin text they are reading, and Hebrew follows its shape.
const hebrewSans = 'Noto Sans Hebrew';
const hebrewSerif = 'Noto Serif Hebrew';
const defaultFontFamily = 'Atkinson Hyperlegible';
/// Where to look after [family], or nothing when it is not one of ours.
List<String> fallbackFor(String family) {
for (final font in readerFonts) {
if (font.family == family) return font.fallback;
}
return const [];
}

View file

@ -1,17 +1,11 @@
import '../../l10n/strings.dart';
import '../../mews/document.dart';
import '../fonts.dart';
import '../controllers/reader_settings_controller.dart'
show ReaderSettingsController;
import 'mews_urls.dart';
import 'page_blocks.dart';
/// The bundled typefaces the reader can pick. Adding a font is one TTF in
/// assets/fonts, one entry in pubspec.yaml, one entry here and one label in
/// the locale file.
const _fonts = <({String family, String labelId})>[
(family: 'Atkinson Hyperlegible', labelId: 'settings-font-atkinson'),
];
/// Message ids for the values the controller accepts. Kept literal so every
/// id in the locale file can be found by searching for it.
const _themeLabels = {
@ -43,7 +37,7 @@ Future<MewsPage> buildSettingsPage(
...chrome(title: title, current: url, strings: strings),
HeadingBlock(2, [MewsText(strings.t('settings-font-heading'))]),
choiceRow([
for (final font in _fonts)
for (final font in readerFonts)
(
href: MewsUrls.action('set-font', {'value': font.family}),
label: strings.t(font.labelId),

View file

@ -9,6 +9,7 @@ import '../application/controllers/bookmarks_controller.dart';
import '../application/controllers/browser_controller.dart';
import '../application/controllers/reader_settings_controller.dart';
import '../application/controllers/subscriptions_controller.dart';
import '../application/fonts.dart';
import '../application/internal_actions.dart';
import '../application/internal_pages.dart';
import '../database/database.dart';
@ -171,6 +172,7 @@ class _ThemedApp extends StatelessWidget {
// The chrome reads in the typeface the reader chose for pages, so the
// address bar and the page below it are not two different faces.
fontFamily: fontFamily,
fontFamilyFallback: fallbackFor(fontFamily),
scaffoldBackgroundColor: palette.background,
// Selection highlights and the caret follow the palette too.
textSelectionTheme: TextSelectionThemeData(

View file

@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import '../../l10n/strings.dart';
import '../../application/fonts.dart';
import '../../mews/document.dart';
import 'palettes.dart';
@ -117,6 +118,7 @@ class _MewsPageViewState extends State<MewsPageView> {
style: TextStyle(
color: palette.foreground.withValues(alpha: 0.7),
fontFamily: widget.fontFamily,
fontFamilyFallback: fallbackFor(widget.fontFamily),
fontSize: base * 0.85,
),
),
@ -129,6 +131,7 @@ class _MewsPageViewState extends State<MewsPageView> {
final foreground = TextStyle(
color: palette.foreground,
fontFamily: widget.fontFamily,
fontFamilyFallback: fallbackFor(widget.fontFamily),
fontSize: base,
height: 1.5,
);

View file

@ -120,6 +120,9 @@ subscriptions-tagged = Showing sites tagged { $tag }
settings-page-title = Settings
settings-font-heading = Typeface
settings-font-atkinson = Atkinson Hyperlegible
settings-font-luciole = Luciole
settings-font-literata = Literata
settings-font-charis = Charis SIL
settings-size-heading = Text size
settings-size-decrease = Smaller text
settings-size-increase = Larger text

View file

@ -46,3 +46,43 @@ flutter:
- asset: assets/fonts/AtkinsonHyperlegible-BoldItalic.ttf
weight: 700
style: italic
- family: Luciole
fonts:
- asset: assets/fonts/Luciole-Regular.ttf
- asset: assets/fonts/Luciole-Italic.ttf
style: italic
- asset: assets/fonts/Luciole-Bold.ttf
weight: 700
- asset: assets/fonts/Luciole-BoldItalic.ttf
weight: 700
style: italic
- family: Literata
fonts:
- asset: assets/fonts/Literata-Regular.ttf
- asset: assets/fonts/Literata-Italic.ttf
style: italic
- asset: assets/fonts/Literata-Bold.ttf
weight: 700
- asset: assets/fonts/Literata-BoldItalic.ttf
weight: 700
style: italic
- family: Charis SIL
fonts:
- asset: assets/fonts/Charis-Regular.ttf
- asset: assets/fonts/Charis-Italic.ttf
style: italic
- asset: assets/fonts/Charis-Bold.ttf
weight: 700
- asset: assets/fonts/Charis-BoldItalic.ttf
weight: 700
style: italic
- family: Noto Sans Hebrew
fonts:
- asset: assets/fonts/NotoSansHebrew-Regular.ttf
- asset: assets/fonts/NotoSansHebrew-Bold.ttf
weight: 700
- family: Noto Serif Hebrew
fonts:
- asset: assets/fonts/NotoSerifHebrew-Regular.ttf
- asset: assets/fonts/NotoSerifHebrew-Bold.ttf
weight: 700

98
app/test/fonts_test.dart Normal file
View file

@ -0,0 +1,98 @@
import 'dart:io';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mews/application/controllers/reader_settings_controller.dart';
import 'package:mews/application/fonts.dart';
import 'package:mews/database/database.dart';
/// The families pubspec.yaml declares, with the asset paths of each.
Map<String, List<String>> _declaredFonts() {
final lines = File('pubspec.yaml').readAsLinesSync();
final declared = <String, List<String>>{};
String? family;
for (final line in lines) {
final name = RegExp(r'^ - family: (.+)$').firstMatch(line);
if (name != null) {
family = name.group(1)!.trim();
declared[family] = [];
continue;
}
final asset = RegExp(r'^ - asset: (.+)$').firstMatch(line);
if (asset != null && family != null) {
declared[family]!.add(asset.group(1)!.trim());
}
}
return declared;
}
void main() {
test('every offered typeface is bundled under the name it is stored by', () {
final declared = _declaredFonts();
for (final font in readerFonts) {
// A family named here but not in pubspec renders in the platform's font
// instead, silently: the reader would pick it and see no change.
expect(declared, contains(font.family),
reason: '${font.family} is offered but not declared in pubspec.yaml');
expect(declared[font.family], hasLength(4),
reason: '${font.family} needs regular, italic, bold and bold italic');
for (final asset in declared[font.family]!) {
expect(File(asset).existsSync(), isTrue, reason: '$asset is missing');
}
}
});
test('a fallback only names a typeface that is bundled', () {
final declared = _declaredFonts();
for (final font in readerFonts) {
for (final family in font.fallback) {
// A fallback naming a family the build does not carry is silently
// skipped, so the glyph would come from the platform after all.
expect(declared, contains(family),
reason: '${font.family} falls back to an absent $family');
expect(family, isNot(font.family), reason: 'falls back to itself');
for (final asset in declared[family]!) {
expect(File(asset).existsSync(), isTrue, reason: '$asset is missing');
}
}
}
expect(readerFonts.map((f) => f.family), contains(defaultFontFamily));
expect(fallbackFor('Comic Sans MS'), isEmpty);
});
test('every typeface can reach Hebrew without the platform', () {
// Hebrew ships from the start, so it must never depend on what the device
// happens to have installed.
for (final font in readerFonts) {
expect(font.fallback, anyElement(anyOf(hebrewSans, hebrewSerif)),
reason: '${font.family} has no Hebrew to fall back on');
}
});
test('every typeface has a label of its own', () {
final ftl = File('locales/en-US/main.ftl').readAsStringSync();
final ids = <String>{};
for (final font in readerFonts) {
expect(ftl, contains('${font.labelId} ='),
reason: '${font.family} has no label');
expect(ids.add(font.labelId), isTrue, reason: 'duplicate label id');
}
});
test('a typeface this build does not ship is refused', () async {
final db = MewsDatabase.test(NativeDatabase.memory());
addTearDown(db.close);
final settings = ReaderSettingsController(db);
expect(await settings.setFont('Literata'), isTrue);
expect(settings.font.value, 'Literata');
// Comic Sans is not bundled; accepting it would render in the platform
// font while the settings page claimed otherwise.
expect(await settings.setFont('Comic Sans MS'), isFalse);
expect(settings.font.value, 'Literata');
expect(await settings.setFont(''), isFalse);
expect(settings.font.value, 'Literata');
});
}