Default to the system monospace font
The default was Berkeley Mono, which is one machine's licensed font and
reads as configuration rather than a default. It is now the system's
monospace font, so a fresh install looks right anywhere and --font is
there for anyone who wants their own.
Getting "the system monospace font" is less direct than it sounds.
Family::Name("monospace") resolves to Noto Sans — not monospaced at all.
cosmic-text's generic Family::Monospace goes through fontdb's built-in
preference, "FreeMono", which is usually absent, and then lands on an
arbitrary face (Adwaita Mono here). Enabling cosmic-text's fontconfig
feature changes nothing, because fontdb's config parser does not pick up
the alias files a distribution actually ships.
So fontconfig is asked directly: `fc-match -f %{family} monospace`, which
is the same answer every other application on the system gets — Noto Sans
Mono here. It costs 10ms on the worker thread that is already waiting for
the compositor, so nothing in wall clock. Without the fontconfig tools a
short list of common distribution defaults is tried instead.
Weight and stretch overrides went too: they were tuned for Berkeley Mono
Medium SemiCondensed, and a family name carries that anyway — the full
name resolves to exactly that face, verified by rendering both.
--verbose now reports the family the labels were shaped with, since the
default legitimately differs from machine to machine.
This commit is contained in:
@@ -46,7 +46,7 @@ wl-pick [--format tsv|json|portal] [--live all|current|none] [--fps N]
|
|||||||
- `--fps N` cap on live updates per tile per second (default 12)
|
- `--fps N` cap on live updates per tile per second (default 12)
|
||||||
- `--no-outputs` windows only; displays are included as tiles by default
|
- `--no-outputs` windows only; displays are included as tiles by default
|
||||||
- `--hide-labels` draws an icon-only grid
|
- `--hide-labels` draws an icon-only grid
|
||||||
- `--font FAMILY` label font family (default `Berkeley Mono`)
|
- `--font FAMILY` label font family (default: the system monospace font)
|
||||||
- `--font-size PX` label size in logical px
|
- `--font-size PX` label size in logical px
|
||||||
- `--timeout SECS` exits after a deadline, in case the keyboard grab ever traps
|
- `--timeout SECS` exits after a deadline, in case the keyboard grab ever traps
|
||||||
you
|
you
|
||||||
@@ -143,10 +143,18 @@ columns capped at 4, 16:9 tiles, `title · app` centred underneath) and live in
|
|||||||
`src/theme.rs`, which is the one place to change them. They are not
|
`src/theme.rs`, which is the one place to change them. They are not
|
||||||
configurable at runtime beyond the font flags.
|
configurable at runtime beyond the font flags.
|
||||||
|
|
||||||
The font is looked up by family name. Your own font directories are scanned
|
The label font defaults to the system monospace font — whatever `fc-match
|
||||||
first because they are small; the full system scan (~37ms) happens only if the
|
monospace` answers, which is what the rest of the desktop uses. (cosmic-text's
|
||||||
family isn't found there, and an unknown family then falls back to whatever
|
own generic resolves through a built-in preference that is usually not
|
||||||
cosmic-text picks rather than failing. Long titles are ellipsised to the cell.
|
installed, and then lands on an arbitrary face, so it is asked directly
|
||||||
|
instead; if fontconfig isn't available, a short list of common distribution
|
||||||
|
defaults is tried.) `--font` names a family instead, and `--verbose` reports
|
||||||
|
which family the labels were actually shaped with.
|
||||||
|
|
||||||
|
Naming a family scans your own font directories first because they are small;
|
||||||
|
the full system scan (~37ms) happens only if it isn't found there. An unknown
|
||||||
|
family falls back to whatever cosmic-text picks rather than failing. Long titles
|
||||||
|
are ellipsised to the cell.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -221,7 +221,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"wl-pick: {} tile(s), {} captured; grid {}x{}, surface {}x{} logical \
|
"wl-pick: {} tile(s), {} captured; grid {}x{}, surface {}x{} logical \
|
||||||
at scale {}, {} MB of capture buffers",
|
at scale {}, {} MB of capture buffers, labels in {:?}",
|
||||||
self.tiles.len(),
|
self.tiles.len(),
|
||||||
self.tiles.iter().filter(|t| t.ready).count(),
|
self.tiles.iter().filter(|t| t.ready).count(),
|
||||||
self.layout.cols,
|
self.layout.cols,
|
||||||
@@ -230,6 +230,7 @@ impl App {
|
|||||||
self.layout.height,
|
self.layout.height,
|
||||||
self.scale,
|
self.scale,
|
||||||
self.stats.pool_bytes >> 20,
|
self.stats.pool_bytes >> 20,
|
||||||
|
self.labels.as_ref().map(|l| l.family()).unwrap_or("none"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ usage: wl-pick [options]
|
|||||||
--fps N cap on live updates per tile per second [12]
|
--fps N cap on live updates per tile per second [12]
|
||||||
--no-outputs windows only; displays are included by default
|
--no-outputs windows only; displays are included by default
|
||||||
--hide-labels draw an icon-only grid
|
--hide-labels draw an icon-only grid
|
||||||
--font FAMILY label font family [Berkeley Mono]
|
--font FAMILY label font family [the system monospace font]
|
||||||
--font-size PX label size in logical px [13.3]
|
--font-size PX label size in logical px [13.3]
|
||||||
--timeout SECS exit anyway after SECS, in case the keyboard
|
--timeout SECS exit anyway after SECS, in case the keyboard
|
||||||
grab ever traps you [off]
|
grab ever traps you [off]
|
||||||
|
|||||||
+102
-21
@@ -12,8 +12,7 @@
|
|||||||
use std::thread::{self, JoinHandle};
|
use std::thread::{self, JoinHandle};
|
||||||
|
|
||||||
use cosmic_text::{
|
use cosmic_text::{
|
||||||
Align, Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, Stretch, SwashCache, Weight,
|
Align, Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, SwashCache, Wrap, fontdb,
|
||||||
Wrap, fontdb,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::shm::Painter;
|
use crate::shm::Painter;
|
||||||
@@ -23,6 +22,7 @@ pub struct Labels {
|
|||||||
fs: FontSystem,
|
fs: FontSystem,
|
||||||
cache: SwashCache,
|
cache: SwashCache,
|
||||||
lines: Vec<Buffer>,
|
lines: Vec<Buffer>,
|
||||||
|
family: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shape `texts` into one centred single line each, at most `box_w` wide.
|
/// Shape `texts` into one centred single line each, at most `box_w` wide.
|
||||||
@@ -36,37 +36,94 @@ pub fn spawn(
|
|||||||
thread::spawn(move || build(texts, family, font_px, line_h, box_w))
|
thread::spawn(move || build(texts, family, font_px, line_h, box_w))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The default family: whatever this system calls its monospace font.
|
||||||
|
pub const SYSTEM_MONO: &str = "monospace";
|
||||||
|
|
||||||
|
/// Families to try when fontconfig cannot be asked, roughly in order of how
|
||||||
|
/// likely a distribution is to ship one as its default monospace.
|
||||||
|
const MONO_CANDIDATES: &[&str] = &[
|
||||||
|
"Noto Sans Mono",
|
||||||
|
"DejaVu Sans Mono",
|
||||||
|
"Liberation Mono",
|
||||||
|
"Adwaita Mono",
|
||||||
|
"Source Code Pro",
|
||||||
|
"Hack",
|
||||||
|
"Fira Mono",
|
||||||
|
"Courier New",
|
||||||
|
];
|
||||||
|
|
||||||
/// Load the smallest font database that can render `family`.
|
/// Load the smallest font database that can render `family`.
|
||||||
///
|
///
|
||||||
/// `FontSystem::new()` scans every system font, which costs ~37ms — most of the
|
/// `FontSystem::new()` scans every system font, which costs ~37ms — most of the
|
||||||
/// startup budget. A user's own font directories are tiny by comparison, so try
|
/// startup budget. A user's own font directories are tiny by comparison, so try
|
||||||
/// those first and only pay for the full scan when the family really isn't
|
/// those first and only pay for the full scan when the family really isn't there
|
||||||
/// there (which is also what makes an unknown family fall back gracefully).
|
/// (which is also what makes an unknown family fall back gracefully). The
|
||||||
|
/// generic default lives among the system fonts, so it skips that shortcut.
|
||||||
fn font_db(family: &str) -> FontSystem {
|
fn font_db(family: &str) -> FontSystem {
|
||||||
let mut db = fontdb::Database::new();
|
let mut db = fontdb::Database::new();
|
||||||
if let Ok(home) = std::env::var("HOME") {
|
if !is_generic(family) {
|
||||||
db.load_fonts_dir(format!("{home}/.fonts"));
|
if let Ok(home) = std::env::var("HOME") {
|
||||||
db.load_fonts_dir(format!("{home}/.local/share/fonts"));
|
db.load_fonts_dir(format!("{home}/.fonts"));
|
||||||
|
db.load_fonts_dir(format!("{home}/.local/share/fonts"));
|
||||||
|
}
|
||||||
|
if has_family(&db, family) {
|
||||||
|
// The locale only orders CJK fallbacks; labels are ids and titles.
|
||||||
|
return FontSystem::new_with_locale_and_db("en-US".to_string(), db);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let found = db
|
db.load_system_fonts();
|
||||||
.faces()
|
|
||||||
.any(|f| f.families.iter().any(|(name, _)| name == family));
|
|
||||||
if !found {
|
|
||||||
db.load_system_fonts();
|
|
||||||
}
|
|
||||||
// The locale only orders CJK fallbacks; labels here are app ids and titles.
|
|
||||||
FontSystem::new_with_locale_and_db("en-US".to_string(), db)
|
FontSystem::new_with_locale_and_db("en-US".to_string(), db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_generic(family: &str) -> bool {
|
||||||
|
family.eq_ignore_ascii_case(SYSTEM_MONO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_family(db: &fontdb::Database, family: &str) -> bool {
|
||||||
|
db.faces()
|
||||||
|
.any(|f| f.families.iter().any(|(name, _)| name == family))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn the generic default into a real family name.
|
||||||
|
///
|
||||||
|
/// cosmic-text's own generic resolves through fontdb's built-in preference
|
||||||
|
/// ("FreeMono"), which is usually absent and then lands on an arbitrary face — so
|
||||||
|
/// ask fontconfig instead, since that is what the rest of the desktop uses. A
|
||||||
|
/// named family passes through untouched; if it turns out to be missing,
|
||||||
|
/// cosmic-text falls back on its own.
|
||||||
|
fn resolve_family(db: &fontdb::Database, family: &str) -> String {
|
||||||
|
if !is_generic(family) {
|
||||||
|
return family.to_string();
|
||||||
|
}
|
||||||
|
fc_match_mono()
|
||||||
|
.filter(|name| has_family(db, name))
|
||||||
|
.or_else(|| {
|
||||||
|
MONO_CANDIDATES
|
||||||
|
.iter()
|
||||||
|
.find(|name| has_family(db, name))
|
||||||
|
.map(|name| name.to_string())
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| family.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What fontconfig says "monospace" means here. A system without the fontconfig
|
||||||
|
/// tools is not an error: the candidate list covers the common defaults.
|
||||||
|
fn fc_match_mono() -> Option<String> {
|
||||||
|
let out = std::process::Command::new("fc-match")
|
||||||
|
.args(["-f", "%{family}", SYSTEM_MONO])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
let text = String::from_utf8(out.stdout).ok()?;
|
||||||
|
// fc-match can answer with several comma-separated aliases for one face.
|
||||||
|
let first = text.split(',').next()?.trim().to_string();
|
||||||
|
(!first.is_empty()).then_some(first)
|
||||||
|
}
|
||||||
|
|
||||||
fn build(texts: Vec<String>, family: String, font_px: f32, line_h: f32, box_w: f32) -> Labels {
|
fn build(texts: Vec<String>, family: String, font_px: f32, line_h: f32, box_w: f32) -> Labels {
|
||||||
let mut fs = font_db(&family);
|
let mut fs = font_db(&family);
|
||||||
let mut cache = SwashCache::new();
|
let mut cache = SwashCache::new();
|
||||||
// An unknown family is not an error: cosmic-text falls back to a system
|
let family = resolve_family(fs.db(), &family);
|
||||||
// face, which is the whole reason the font is named rather than pathed.
|
let attrs = Attrs::new().family(Family::Name(&family));
|
||||||
let attrs = Attrs::new()
|
|
||||||
.family(Family::Name(&family))
|
|
||||||
.weight(Weight(500))
|
|
||||||
.stretch(Stretch::SemiCondensed);
|
|
||||||
let metrics = Metrics::new(font_px, line_h);
|
let metrics = Metrics::new(font_px, line_h);
|
||||||
|
|
||||||
let mut lines = Vec::with_capacity(texts.len());
|
let mut lines = Vec::with_capacity(texts.len());
|
||||||
@@ -80,7 +137,12 @@ fn build(texts: Vec<String>, family: String, font_px: f32, line_h: f32, box_w: f
|
|||||||
buf.draw(&mut fs, &mut cache, Color::rgb(0, 0, 0), |_, _, _, _, _| {});
|
buf.draw(&mut fs, &mut cache, Color::rgb(0, 0, 0), |_, _, _, _, _| {});
|
||||||
lines.push(buf);
|
lines.push(buf);
|
||||||
}
|
}
|
||||||
Labels { fs, cache, lines }
|
Labels {
|
||||||
|
fs,
|
||||||
|
cache,
|
||||||
|
lines,
|
||||||
|
family,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shorten `text` until it fits in `box_w`, ending with an ellipsis — window
|
/// Shorten `text` until it fits in `box_w`, ending with an ellipsis — window
|
||||||
@@ -124,6 +186,12 @@ fn ellipsize(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Labels {
|
impl Labels {
|
||||||
|
/// The family the labels were actually shaped with, which for the generic
|
||||||
|
/// default depends on what this system has installed.
|
||||||
|
pub fn family(&self) -> &str {
|
||||||
|
&self.family
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw label `i` inside `at` (physical px), clipped to it.
|
/// Draw label `i` inside `at` (physical px), clipped to it.
|
||||||
pub fn draw(&mut self, p: &mut Painter, i: usize, at: Rect, color: Argb) {
|
pub fn draw(&mut self, p: &mut Painter, i: usize, at: Rect, color: Argb) {
|
||||||
let Some(buf) = self.lines.get_mut(i) else {
|
let Some(buf) = self.lines.get_mut(i) else {
|
||||||
@@ -171,6 +239,19 @@ mod tests {
|
|||||||
assert!(touched > 20, "only {touched} pixels were painted");
|
assert!(touched > 20, "only {touched} pixels were painted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_generic_default_resolves_to_a_real_monospace_family() {
|
||||||
|
let fs = font_db(SYSTEM_MONO);
|
||||||
|
let resolved = resolve_family(fs.db(), SYSTEM_MONO);
|
||||||
|
assert_ne!(resolved, SYSTEM_MONO, "should have named a real family");
|
||||||
|
assert!(
|
||||||
|
has_family(fs.db(), &resolved),
|
||||||
|
"{resolved:?} is not in the database"
|
||||||
|
);
|
||||||
|
// A named family passes through, present or not.
|
||||||
|
assert_eq!(resolve_family(fs.db(), "Some Font"), "Some Font");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ellipsizes_long_titles() {
|
fn ellipsizes_long_titles() {
|
||||||
let mut fs = FontSystem::new();
|
let mut fs = FontSystem::new();
|
||||||
|
|||||||
+5
-4
@@ -29,9 +29,10 @@ pub struct Theme {
|
|||||||
pub max_cols: i32,
|
pub max_cols: i32,
|
||||||
/// Gap between a thumbnail and its label (rasi `element { spacing }`).
|
/// Gap between a thumbnail and its label (rasi `element { spacing }`).
|
||||||
pub spacing: i32,
|
pub spacing: i32,
|
||||||
/// Label font: a family name resolved against system fonts, with whatever
|
/// Label font family, resolved against the system's fonts. The default is
|
||||||
/// cosmic-text falls back to if it is missing. Size and line height are
|
/// the generic "monospace", which becomes whatever fontconfig says that is
|
||||||
/// logical px — rofi's "Berkeley Mono 12" at pango size="small".
|
/// here. Size and line height are logical px, matching the rofi theme the
|
||||||
|
/// look came from (12pt at pango size="small").
|
||||||
pub font: String,
|
pub font: String,
|
||||||
pub font_px: f32,
|
pub font_px: f32,
|
||||||
pub line_h: i32,
|
pub line_h: i32,
|
||||||
@@ -55,7 +56,7 @@ impl Default for Theme {
|
|||||||
margin: 12,
|
margin: 12,
|
||||||
max_cols: 4,
|
max_cols: 4,
|
||||||
spacing: 10,
|
spacing: 10,
|
||||||
font: "Berkeley Mono".to_string(),
|
font: crate::text::SYSTEM_MONO.to_string(),
|
||||||
font_px: 13.3,
|
font_px: 13.3,
|
||||||
line_h: 17,
|
line_h: 17,
|
||||||
labels: true,
|
labels: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user