Compare commits
5
Commits
314e474341
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ce2854d72 | ||
|
|
e1d8760a43 | ||
|
|
f78fd7ee41 | ||
|
|
6559956c3c | ||
|
|
77efdf03e9 |
+1
-1
@@ -5,7 +5,7 @@ edition = "2024"
|
||||
description = "An simple alt-tab window switcher for sway"
|
||||
license = "MIT"
|
||||
repository = "https://git.krzak.org/N0VA/wl-tab"
|
||||
readme = "README.md"
|
||||
readme = "README.adoc"
|
||||
keywords = ["wayland", "sway", "wlroots", "alt-tab", "switcher"]
|
||||
categories = ["command-line-utilities", "os::unix-apis"]
|
||||
rust-version = "1.89"
|
||||
|
||||
+3
-1
@@ -1,2 +1,4 @@
|
||||
= wl-tab
|
||||
== A simple alt-tab switcher for sway
|
||||
A simple alt-tab switcher for sway
|
||||
|
||||
Originally forked from https://github.com/mil-ad/wl-pick[wl-pick]
|
||||
|
||||
+5
-5
@@ -161,7 +161,7 @@ impl Ending {
|
||||
Ending::Picked => "picked",
|
||||
Ending::Cancelled => "cancelled",
|
||||
Ending::Closed => "the compositor closed the overlay",
|
||||
Ending::Unfocused => "lost the keyboard to another surface",
|
||||
Ending::Unfocused => "another surface holds the keyboard",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,7 +260,7 @@ impl App {
|
||||
eprintln!(" [{i}] {}{mark}", t.target.tsv());
|
||||
}
|
||||
eprintln!(
|
||||
"wl-pick: {} tile(s), {} captured; grid {}x{}, surface {}x{} logical \
|
||||
"wl-tab: {} tile(s), {} captured; grid {}x{}, surface {}x{} logical \
|
||||
at scale {}, {} MB of capture buffers, labels in {:?}",
|
||||
self.tiles.len(),
|
||||
self.tiles.iter().filter(|t| t.ready).count(),
|
||||
@@ -274,7 +274,7 @@ impl App {
|
||||
);
|
||||
if self.layout.scrollable() {
|
||||
eprintln!(
|
||||
"wl-pick: {} of {} rows fit; the rest scroll",
|
||||
"wl-tab: {} of {} rows fit; the rest scroll",
|
||||
self.layout.visible_rows, self.layout.rows
|
||||
);
|
||||
}
|
||||
@@ -285,7 +285,7 @@ impl App {
|
||||
let frames: u32 = self.tiles.iter().map(|t| t.frames).sum();
|
||||
let secs = open_for.as_secs_f64();
|
||||
eprintln!(
|
||||
"wl-pick: {}, {frames} frame(s) over {secs:.1}s = {:.1}/s, {} tick(s), \
|
||||
"wl-tab: {}, {frames} frame(s) over {secs:.1}s = {:.1}/s, {} tick(s), \
|
||||
{} starved; per tile: {}",
|
||||
self.ending.as_str(),
|
||||
frames as f64 / secs,
|
||||
@@ -314,7 +314,7 @@ impl App {
|
||||
.map(|t| t.target.title.as_str())
|
||||
.collect();
|
||||
eprintln!(
|
||||
"wl-pick: no frame for {} of {} tiles ({}); \
|
||||
"wl-tab: no frame for {} of {} tiles ({}); \
|
||||
another capture client may hold these sources",
|
||||
stuck.len(),
|
||||
self.tiles.len(),
|
||||
|
||||
+2
-2
@@ -201,7 +201,7 @@ impl App {
|
||||
self.stats.pool_bytes = total;
|
||||
// Note: no mmap. The compositor writes these pages and samples them
|
||||
// again for display; mapping them here would only cost us the faults.
|
||||
let file = shm::memfd("wl-pick-capture", total)?;
|
||||
let file = shm::memfd("wl-tab-capture", total)?;
|
||||
let pool = self.shm.create_pool(file.as_fd(), total as i32, qh, ());
|
||||
for (i, slot_offsets) in offsets.iter().enumerate() {
|
||||
let (w, h, format) = {
|
||||
@@ -372,7 +372,7 @@ impl Dispatch<ExtImageCopyCaptureFrameV1, usize> for App {
|
||||
// frame yet leaves the tile without a thumbnail.
|
||||
if tile.frames == 0 {
|
||||
eprintln!(
|
||||
"wl-pick: capture failed for {:?} ({reason:?})",
|
||||
"wl-tab: capture failed for {:?} ({reason:?})",
|
||||
tile.target.title
|
||||
);
|
||||
}
|
||||
|
||||
+8
-7
@@ -10,17 +10,18 @@ use crate::target::Format;
|
||||
use crate::theme::Theme;
|
||||
|
||||
const HELP: &str = "\
|
||||
wl-rab - a simple alt-tab switcher for sway
|
||||
wl-tab - a simple alt-tab switcher for sway
|
||||
|
||||
usage: wl-tab [options]
|
||||
|
||||
--config PATH config file [~/.config/wl-pick/config]
|
||||
--config PATH config file [~/.config/wl-tab/config]
|
||||
--format tsv|json|portal how to report the pick [tsv]
|
||||
--fps N cap on live updates per tile per second [12]
|
||||
--labels, --no-labels a label under each thumbnail [yes]
|
||||
--order mru|tree window ordering: mru or layout tree [mru]
|
||||
--focus, --no-focus focus the picked target directly [no]
|
||||
--font FAMILY label font family [the system monospace font]
|
||||
--font NAME label font family, optionally with a style, as
|
||||
in 'FiraCode Mono' [system monospace]
|
||||
--font-size PX label size in logical px [13.3]
|
||||
--timeout SECS exit anyway after SECS, in case the keyboard
|
||||
grab ever traps you [off]
|
||||
@@ -47,7 +48,7 @@ config:
|
||||
max-width = 90ppt # the box the grid may fill
|
||||
max-height = 90ppt
|
||||
|
||||
font = monospace # also --font
|
||||
font = monospace # a family, optionally with a style
|
||||
font-size = 13.3
|
||||
labels = yes
|
||||
outputs = no # include whole displays as tiles
|
||||
@@ -78,7 +79,7 @@ formats:
|
||||
|
||||
[screencast]
|
||||
chooser_type=simple
|
||||
chooser_cmd=wl-pick --format portal";
|
||||
chooser_cmd=wl-tab --format portal";
|
||||
|
||||
/// What the command line asked for. Every setting is optional so the config file
|
||||
/// can fill the gaps: a flag beats the file, the file beats the default.
|
||||
@@ -117,7 +118,7 @@ pub fn arm_timeout(timeout: Option<Duration>) {
|
||||
if let Some(d) = timeout {
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(d);
|
||||
eprintln!("wl-pick: timeout");
|
||||
eprintln!("wl-tab: timeout");
|
||||
std::process::exit(2);
|
||||
});
|
||||
}
|
||||
@@ -218,7 +219,7 @@ fn parse(it: impl Iterator<Item = String>) -> Result<Args, String> {
|
||||
let v = it.next().ok_or("--fps needs a number")?;
|
||||
args.fps = Some(v.parse().map_err(|_| format!("bad --fps: {v}"))?);
|
||||
}
|
||||
"--font" => args.font = Some(it.next().ok_or("--font needs a family name")?),
|
||||
"--font" => args.font = Some(it.next().ok_or("--font needs a font name")?),
|
||||
"--font-size" => {
|
||||
let v = it.next().ok_or("--font-size needs px")?;
|
||||
args.font_size = Some(v.parse().map_err(|_| format!("bad --font-size: {v}"))?);
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
//! The config file: `~/.config/wl-pick/config`.
|
||||
//! The config file: `~/.config/wl-tab/config`.
|
||||
//!
|
||||
//! Flat `key = value` lines with `#` comments - no sections, no nesting, so a
|
||||
//! TOML parser would be a dependency bought for nothing. Every setting is
|
||||
@@ -186,13 +186,13 @@ fn number<T: std::str::FromStr>(s: &str) -> Result<T, String> {
|
||||
.map_err(|_| format!("{s:?} is not a number"))
|
||||
}
|
||||
|
||||
/// `$XDG_CONFIG_HOME/wl-pick/config`, or `~/.config/wl-pick/config`.
|
||||
/// `$XDG_CONFIG_HOME/wl-tab/config`, or `~/.config/wl-tab/config`.
|
||||
fn default_path() -> PathBuf {
|
||||
let dir = std::env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
|
||||
.unwrap_or_default();
|
||||
dir.join("wl-pick").join("config")
|
||||
dir.join("wl-tab").join("config")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -288,7 +288,7 @@ order = mru
|
||||
|
||||
#[test]
|
||||
fn a_missing_file_is_not_an_error() {
|
||||
let missing = Path::new("/nonexistent/wl-pick/config");
|
||||
let missing = Path::new("/nonexistent/wl-tab/config");
|
||||
assert!(
|
||||
Config::load(Some(missing)).is_err(),
|
||||
"named file must exist"
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
//! wl-pick shows a live grid of every window and display as a layer-shell
|
||||
//! wl-tab shows a live grid of every window and display as a layer-shell
|
||||
//! overlay and reports which one you picked. That is all it does: acting on the
|
||||
//! choice belongs to whatever called it.
|
||||
//!
|
||||
@@ -61,7 +61,7 @@ fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(code) => code,
|
||||
Err(e) => {
|
||||
eprintln!("wl-pick: {e}");
|
||||
eprintln!("wl-tab: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,7 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
|
||||
phases.mark("mapped");
|
||||
|
||||
// The keyboard grab is what makes the overlay usable, so losing it for
|
||||
// good ends the run: that is how a second wl-pick, started from the same
|
||||
// good ends the run: that is how a second wl-tab, started from the same
|
||||
// keybinding, replaces the first instead of leaving it stranded on screen.
|
||||
// A leave only counts once it has failed to come back, because sway also
|
||||
// cycles focus off and on in a single batch as the pointer crosses us.
|
||||
@@ -259,7 +259,7 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
|
||||
// Only the portal format can fail to name something: it identifies a
|
||||
// window by its foreign-toplevel identifier, and this one has none.
|
||||
None => {
|
||||
eprintln!("wl-pick: {:?} has no toplevel identifier", target.title);
|
||||
eprintln!("wl-tab: {:?} has no toplevel identifier", target.title);
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -103,7 +103,7 @@ impl App {
|
||||
&surface,
|
||||
output,
|
||||
Layer::Overlay,
|
||||
"wl-pick".to_string(),
|
||||
"wl-tab".to_string(),
|
||||
qh,
|
||||
(),
|
||||
);
|
||||
@@ -114,7 +114,7 @@ impl App {
|
||||
|
||||
let (pw, ph) = (lw * self.scale, lh * self.scale);
|
||||
let len = shm::Chrome::slot_len(pw, ph) * shm::Chrome::SLOTS;
|
||||
let file = shm::memfd("wl-pick-chrome", len)?;
|
||||
let file = shm::memfd("wl-tab-chrome", len)?;
|
||||
let pool = self.shm.create_pool(file.as_fd(), len as i32, qh, ());
|
||||
for slot in 0..shm::Chrome::SLOTS {
|
||||
self.chrome_buffers.push(pool.create_buffer(
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
//! Wayland side only supplies pixels. The join between the two is
|
||||
//! `foreign_toplevel_identifier`, which sway reports per view.
|
||||
//!
|
||||
//! Acting on the choice is deliberately not here: wl-pick reports what was picked
|
||||
//! Acting on the choice is deliberately not here: wl-tab reports what was picked
|
||||
//! and the caller decides what that means.
|
||||
|
||||
use std::os::unix::net::UnixStream;
|
||||
@@ -33,7 +33,7 @@ pub fn connect() -> Result<Connection, String> {
|
||||
let live = live_sockets();
|
||||
let [path] = live.as_slice() else {
|
||||
return Err(if live.is_empty() {
|
||||
"cannot reach sway; wl-pick reads the window list from its IPC \
|
||||
"cannot reach sway; wl-tab reads the window list from its IPC \
|
||||
socket, and no running sway has one"
|
||||
.to_string()
|
||||
} else {
|
||||
@@ -123,7 +123,7 @@ fn history_path() -> PathBuf {
|
||||
std::env::var_os("XDG_RUNTIME_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(std::env::temp_dir)
|
||||
.join("wl-pick-history")
|
||||
.join("wl-tab-history")
|
||||
}
|
||||
|
||||
pub fn record_focus(con_id: i64) {
|
||||
|
||||
+241
-27
@@ -1,8 +1,8 @@
|
||||
//! Labels.
|
||||
//!
|
||||
//! Building a font system and rasterising the first glyphs costs ~55ms, which is
|
||||
//! almost exactly the window the compositor spends copying window pixels back
|
||||
//! for us. So all of it happens on a worker thread started before the captures
|
||||
//! Building a font system and rasterising the first glyphs costs ~55ms, about
|
||||
//! what the compositor spends copying window pixels back for a handful of
|
||||
//! windows. So all of it happens on a worker thread started before the captures
|
||||
//! and joined after them: by the time anything is drawn, every label is shaped
|
||||
//! and its glyphs are already in the cache, and painting one costs ~0.1ms.
|
||||
//!
|
||||
@@ -12,7 +12,8 @@
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use cosmic_text::{
|
||||
Align, Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, SwashCache, Wrap, fontdb,
|
||||
Align, Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, Stretch, SwashCache, Weight,
|
||||
Wrap, fontdb,
|
||||
};
|
||||
|
||||
use crate::shm::Painter;
|
||||
@@ -66,7 +67,7 @@ fn font_db(family: &str) -> FontSystem {
|
||||
db.load_fonts_dir(format!("{home}/.fonts"));
|
||||
db.load_fonts_dir(format!("{home}/.local/share/fonts"));
|
||||
}
|
||||
if has_family(&db, family) {
|
||||
if interpret(&db, family).is_some() {
|
||||
// The locale only orders CJK fallbacks; labels are ids and titles.
|
||||
return FontSystem::new_with_locale_and_db("en-US".to_string(), db);
|
||||
}
|
||||
@@ -79,31 +80,178 @@ fn is_generic(family: &str) -> bool {
|
||||
family.eq_ignore_ascii_case(SYSTEM_MONO)
|
||||
}
|
||||
|
||||
fn has_family(db: &fontdb::Database, family: &str) -> bool {
|
||||
fn family_named(db: &fontdb::Database, want: &str) -> Option<String> {
|
||||
db.faces()
|
||||
.any(|f| f.families.iter().any(|(name, _)| name == family))
|
||||
.flat_map(|face| face.families.iter())
|
||||
.find(|(name, _)| name.eq_ignore_ascii_case(want))
|
||||
.map(|(name, _)| name.clone())
|
||||
}
|
||||
|
||||
/// Turn the generic default into a real family name.
|
||||
/// A resolved family and the face style requested within it.
|
||||
///
|
||||
/// 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();
|
||||
/// ask fontconfig instead, since that is what the rest of the desktop uses.
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Choice {
|
||||
family: String,
|
||||
weight: Weight,
|
||||
stretch: Stretch,
|
||||
}
|
||||
|
||||
impl Choice {
|
||||
fn plain(family: &str) -> Self {
|
||||
Self {
|
||||
family: family.to_string(),
|
||||
weight: Weight::NORMAL,
|
||||
stretch: Stretch::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
fn attrs(&self) -> Attrs<'_> {
|
||||
Attrs::new()
|
||||
.family(Family::Name(&self.family))
|
||||
.weight(self.weight)
|
||||
.stretch(self.stretch)
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
let mut parts = vec![self.family.as_str()];
|
||||
if self.weight != Weight::NORMAL {
|
||||
parts.extend(spelling(WEIGHTS, self.weight));
|
||||
}
|
||||
if self.stretch != Stretch::Normal {
|
||||
parts.extend(spelling(WIDTHS, self.stretch));
|
||||
}
|
||||
parts.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
fn interpret(db: &fontdb::Database, request: &str) -> Option<Choice> {
|
||||
split_request(request, |name| family_named(db, name))
|
||||
}
|
||||
|
||||
fn split_request(request: &str, lookup: impl Fn(&str) -> Option<String>) -> Option<Choice> {
|
||||
let words: Vec<&str> = request.split_whitespace().collect();
|
||||
for split in (1..=words.len()).rev() {
|
||||
let Some(family) = lookup(&words[..split].join(" ")) else {
|
||||
continue;
|
||||
};
|
||||
let mut choice = Choice::plain(&family);
|
||||
if read_style(&words[split..], &mut choice) {
|
||||
return Some(choice);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn read_style(words: &[&str], choice: &mut Choice) -> bool {
|
||||
let mut i = 0;
|
||||
while i < words.len() {
|
||||
let pair = words.get(i..i + 2).map(|two| two.concat());
|
||||
if pair
|
||||
.as_deref()
|
||||
.is_some_and(|pair| apply_style(pair, choice))
|
||||
{
|
||||
i += 2;
|
||||
} else if apply_style(words[i], choice) {
|
||||
i += 1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn apply_style(word: &str, choice: &mut Choice) -> bool {
|
||||
if let Some(weight) = lookup(WEIGHTS, word) {
|
||||
choice.weight = weight;
|
||||
} else if let Some(stretch) = lookup(WIDTHS, word) {
|
||||
choice.stretch = stretch;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn normalise(word: &str) -> String {
|
||||
word.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric())
|
||||
.map(|c| c.to_ascii_lowercase())
|
||||
.collect()
|
||||
}
|
||||
|
||||
const WEIGHTS: &[(&str, Weight)] = &[
|
||||
("Thin", Weight::THIN),
|
||||
("Hairline", Weight::THIN),
|
||||
("ExtraLight", Weight::EXTRA_LIGHT),
|
||||
("UltraLight", Weight::EXTRA_LIGHT),
|
||||
("Light", Weight::LIGHT),
|
||||
("Regular", Weight::NORMAL),
|
||||
("Normal", Weight::NORMAL),
|
||||
("Book", Weight::NORMAL),
|
||||
("Medium", Weight::MEDIUM),
|
||||
("SemiBold", Weight::SEMIBOLD),
|
||||
("DemiBold", Weight::SEMIBOLD),
|
||||
("Bold", Weight::BOLD),
|
||||
("ExtraBold", Weight::EXTRA_BOLD),
|
||||
("UltraBold", Weight::EXTRA_BOLD),
|
||||
("Black", Weight::BLACK),
|
||||
("Heavy", Weight::BLACK),
|
||||
];
|
||||
|
||||
const WIDTHS: &[(&str, Stretch)] = &[
|
||||
("UltraCondensed", Stretch::UltraCondensed),
|
||||
("ExtraCondensed", Stretch::ExtraCondensed),
|
||||
("Condensed", Stretch::Condensed),
|
||||
("SemiCondensed", Stretch::SemiCondensed),
|
||||
("Normal", Stretch::Normal),
|
||||
("SemiExpanded", Stretch::SemiExpanded),
|
||||
("Expanded", Stretch::Expanded),
|
||||
("ExtraExpanded", Stretch::ExtraExpanded),
|
||||
("UltraExpanded", Stretch::UltraExpanded),
|
||||
];
|
||||
|
||||
fn lookup<T: Copy>(table: &[(&str, T)], word: &str) -> Option<T> {
|
||||
let word = normalise(word);
|
||||
table
|
||||
.iter()
|
||||
.find(|(spelling, _)| normalise(spelling) == word)
|
||||
.map(|(_, value)| *value)
|
||||
}
|
||||
|
||||
fn spelling<T: PartialEq>(table: &[(&'static str, T)], value: T) -> Option<&'static str> {
|
||||
table
|
||||
.iter()
|
||||
.find(|(_, known)| *known == value)
|
||||
.map(|(word, _)| *word)
|
||||
}
|
||||
|
||||
fn choose(db: &fontdb::Database, request: &str) -> Choice {
|
||||
if !is_generic(request) {
|
||||
if let Some(choice) = interpret(db, request) {
|
||||
return choice;
|
||||
}
|
||||
let fallback = system_mono(db);
|
||||
eprintln!(
|
||||
"wl-tab: no font matching {request:?}, using {:?}; `fc-match -f '%{{family}}\\n' {request:?}` names the family",
|
||||
fallback.family
|
||||
);
|
||||
return fallback;
|
||||
}
|
||||
system_mono(db)
|
||||
}
|
||||
|
||||
fn system_mono(db: &fontdb::Database) -> Choice {
|
||||
fc_match_mono()
|
||||
.filter(|name| has_family(db, name))
|
||||
.filter(|name| family_named(db, name).is_some())
|
||||
.or_else(|| {
|
||||
MONO_CANDIDATES
|
||||
.iter()
|
||||
.find(|name| has_family(db, name))
|
||||
.find(|name| family_named(db, name).is_some())
|
||||
.map(|name| name.to_string())
|
||||
})
|
||||
.unwrap_or_else(|| family.to_string())
|
||||
.map_or_else(|| Choice::plain(SYSTEM_MONO), |name| Choice::plain(&name))
|
||||
}
|
||||
|
||||
/// What fontconfig says "monospace" means here. A system without the fontconfig
|
||||
@@ -122,8 +270,8 @@ fn fc_match_mono() -> Option<String> {
|
||||
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 cache = SwashCache::new();
|
||||
let family = resolve_family(fs.db(), &family);
|
||||
let attrs = Attrs::new().family(Family::Name(&family));
|
||||
let choice = choose(fs.db(), &family);
|
||||
let attrs = choice.attrs();
|
||||
let metrics = Metrics::new(font_px, line_h);
|
||||
|
||||
let mut lines = Vec::with_capacity(texts.len());
|
||||
@@ -141,7 +289,7 @@ fn build(texts: Vec<String>, family: String, font_px: f32, line_h: f32, box_w: f
|
||||
fs,
|
||||
cache,
|
||||
lines,
|
||||
family,
|
||||
family: choice.name(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,14 +390,80 @@ mod tests {
|
||||
#[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"
|
||||
let choice = choose(fs.db(), SYSTEM_MONO);
|
||||
assert_ne!(
|
||||
choice.family, SYSTEM_MONO,
|
||||
"should have named a real family"
|
||||
);
|
||||
// A named family passes through, present or not.
|
||||
assert_eq!(resolve_family(fs.db(), "Some Font"), "Some Font");
|
||||
assert!(
|
||||
family_named(fs.db(), &choice.family).is_some(),
|
||||
"{:?} is not in the database",
|
||||
choice.family
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_font_falls_back_to_the_default() {
|
||||
let fs = font_db(SYSTEM_MONO);
|
||||
let choice = choose(fs.db(), "No Such Family At All");
|
||||
assert_eq!(choice, choose(fs.db(), SYSTEM_MONO));
|
||||
}
|
||||
|
||||
fn db(families: &[&'static str]) -> impl Fn(&str) -> Option<String> {
|
||||
let families = families.to_vec();
|
||||
move |want| {
|
||||
families
|
||||
.iter()
|
||||
.find(|name| name.eq_ignore_ascii_case(want))
|
||||
.map(|name| name.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_full_font_name_splits_into_family_and_style() {
|
||||
let choice =
|
||||
split_request("Berkeley Mono Medium SemiCondensed", db(&["Berkeley Mono"])).unwrap();
|
||||
assert_eq!(choice.family, "Berkeley Mono");
|
||||
assert_eq!(choice.weight, Weight::MEDIUM);
|
||||
assert_eq!(choice.stretch, Stretch::SemiCondensed);
|
||||
assert_eq!(choice.name(), "Berkeley Mono Medium SemiCondensed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_family_that_ends_in_a_style_word_wins() {
|
||||
let choice =
|
||||
split_request("Fira Code Light", db(&["Fira Code", "Fira Code Light"])).unwrap();
|
||||
assert_eq!(choice.family, "Fira Code Light");
|
||||
assert_eq!(choice.weight, Weight::NORMAL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_words_accept_joined_spaced_and_mixed_case_forms() {
|
||||
let cases = [
|
||||
("Iosevka demibold", Weight::SEMIBOLD, Stretch::Normal),
|
||||
("Iosevka Extra Light", Weight::EXTRA_LIGHT, Stretch::Normal),
|
||||
(
|
||||
"Iosevka ULTRACONDENSED",
|
||||
Weight::NORMAL,
|
||||
Stretch::UltraCondensed,
|
||||
),
|
||||
("Iosevka Bold Condensed", Weight::BOLD, Stretch::Condensed),
|
||||
];
|
||||
for (request, weight, stretch) in cases {
|
||||
let choice = split_request(request, db(&["Iosevka"])).unwrap();
|
||||
assert_eq!(
|
||||
(choice.weight, choice.stretch),
|
||||
(weight, stretch),
|
||||
"{request:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unreadable_trailing_words_do_not_match() {
|
||||
let known = db(&["Berkeley Mono"]);
|
||||
assert_eq!(split_request("Berkeley Mono Nonsense", &known), None);
|
||||
assert_eq!(split_request("Comic Sans", &known), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user