Repository: j0ru/kickoff (language: rust)

Issue:
Fallback Fonts
This is an incredibly minor issue (pretty much just because of aesthetics, e.g. wanting a symbol for the prompt char that's not in the text-font being used), but it looks like font.rs only supports one font (and it looks like refactoring this for fallback fonting would be pretty involved).

Some possible hacks here:
* having a separate (optional?) `prompt_font` config value that's used when present
* allowing `font` to be a list of fontnames, iterate through fonts in order if a glyph is not present
* using built-in fontconfig `list_fonts` to do auto-fallback (probably gross and inelegant, but 'correct')


Discussion / hints:
Hey,
thanks for the interest! The second option sounds like the most elegant solution, maybe introducing a new list for fallback fonts would be the way to go. I'll look into it!

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index be8cae8..740fd20 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -16,9 +16,14 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
 
 [[package]]
 name = "ahash"
-version = "0.4.7"
+version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
+checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+dependencies = [
+ "getrandom 0.2.4",
+ "once_cell",
+ "version_check",
+]
 
 [[package]]
 name = "aho-corasick"
@@ -575,9 +580,9 @@ dependencies = [
 
 [[package]]
 name = "fontdue"
-version = "0.5.2"
+version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c75712fff1702bac51b7eaa5a5ca9f9853b8055ef5906088a32f4fe196595a1d"
+checksum = "f716db304dca2e287fea5c7e3a22dac3eac9050f9bae143aa424ac2815494475"
 dependencies = [
  "hashbrown",
  "ttf-parser",
@@ -749,9 +754,9 @@ checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
 
 [[package]]
 name = "hashbrown"
-version = "0.9.1"
+version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
 dependencies = [
  "ahash",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index fccf311..d8bf3d0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,7 +13,7 @@ keywords = ["wayland", "launcher", "wlroots"]
 [dependencies]
 wayland-client = "0.29.0"
 smithay-client-toolkit = "0.15.2"
-fontdue = "0.5.2"
+fontdue = "0.6.4"
 image = "0.24.0"
 fuzzy-matcher = "0.3.7"
 nix = "0.23.1"
diff --git a/assets/default_config.toml b/assets/default_config.toml
index d30b253..7b29a8a 100644
--- a/assets/default_config.toml
+++ b/assets/default_config.toml
@@ -6,7 +6,8 @@ prompt = ''
 # space between window border and the content in pixel
 padding = 100
 
-font = ''        # otf or ttf only
+# font = ''        # has been deprecated in favour of fonts list
+fonts = ['Noto Sans Mono']     # list of otf or ttf fonts. later elements work as fallback
 font_size = 32.0
 
 [history]
diff --git a/src/config.rs b/src/config.rs
index 550d2fc..9c50590 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -42,7 +42,8 @@ pub struct HistoryConfig {
 pub struct Config {
     pub prompt: String,
     pub padding: u32,
-    pub font: String,
+    pub font: Option<String>,
+    pub fonts: Vec<String>,
     pub font_size: f32,
     pub colors: ColorConfig,
     pub history: HistoryConfig,
@@ -124,7 +125,8 @@ impl Default for Config {
         Config {
             prompt: "".to_owned(),
             padding: 100,
-            font: "".to_owned(),
+            font: None,
+            fonts: vec![],
             font_size: 32.,
             colors: ColorConfig::default(),
             history: HistoryConfig::default(),
diff --git a/src/font.rs b/src/font.rs
index 82faaba..b9b60eb 100644
--- a/src/font.rs
+++ b/src/font.rs
@@ -1,43 +1,66 @@
 use crate::color::Color;
-
+use fontdue::layout::{CoordinateSystem, GlyphRasterConfig, Layout, LayoutSettings, TextStyle};
+use fontdue::Metrics;
+use std::cell::RefCell;
 use std::collections::HashMap;
-use std::io::Read;
-use std::fs::File;
+use std::path::PathBuf;
 
-use fontconfig::Fontconfig;
+use tokio::{
+    fs::File,
+    io::{self, AsyncReadExt},
+};
 
-use fontdue::{layout::GlyphRasterConfig, FontSettings, Metrics};
+use fontconfig::Fontconfig;
 
 use image::{Pixel, RgbaImage};
 
 pub struct Font {
-    font: fontdue::Font,
+    fonts: Vec<fontdue::Font>,
+    layout: RefCell<Layout>,
     scale: f32,
-    glyph_cache: HashMap<GlyphRasterConfig, (Metrics, Vec<u8>)>,
+    glyph_cache: RefCell<HashMap<GlyphRasterConfig, (Metrics, Vec<u8>)>>,
 }
 
 impl Font {
-    pub fn new(name: &str, size: f32) -> Font {
+    pub async fn new(font_names: Vec<String>, size: f32) -> io::Result<Font> {
         let fc = Fontconfig::new().expect("Couldn't load fontconfig");
-        let font_path = fc.find(name, None).expect("Couldn't find font").path;
-        let mut font_file = File::open(font_path.to_str().unwrap()).expect("Couldn't load font file");
-        let mut font_buffer = Vec::new();
-        font_file.read_to_end(&mut font_buffer).expect("Couldn't load font file");
-        Font {
-            font: fontdue::Font::from_bytes(font_buffer, FontSettings::default())
-                .expect("Failed to parse Font"),
-            scale: size,
-            glyph_cache: HashMap::new(),
+        let font_paths: Vec<PathBuf> = font_names
+            .iter()
+            .map(|name| fc.find(name, None).unwrap().path)
+            .collect();
+        let mut font_data = Vec::new();
+
+        for font_path in font_paths {
+            let mut font_buffer = Vec::new();
+            File::open(font_path.to_str().unwrap())
+                .await?
+                .read_to_end(&mut font_buffer)
+                .await?;
+            font_data.push(
+                fontdue::Font::from_bytes(font_buffer, fontdue::FontSettings::default()).unwrap(),
+            );
         }
+
+        Ok(Font {
+            fonts: font_data,
+            layout: RefCell::new(Layout::new(CoordinateSystem::PositiveYDown)),
+            scale: size,
+            glyph_cache: RefCell::new(HashMap::new()),
+        })
     }
 
-    fn render_glyph(&mut self, conf: GlyphRasterConfig) -> (Metrics, Vec<u8>) {
-        if let Some(bitmap) = self.glyph_cache.get(&conf) {
+    fn render_glyph(&self, conf: GlyphRasterConfig) -> (Metrics, Vec<u8>) {
+        let mut glyph_cache = self.glyph_cache.borrow_mut();
+        if let Some(bitmap) = glyph_cache.get(&conf) {
             bitmap.clone()
         } else {
-            self.glyph_cache
-                .insert(conf, self.font.rasterize_config(conf));
-            self.glyph_cache.get(&conf).unwrap().clone()
+            let font: Vec<&fontdue::Font> = self
+                .fonts
+                .iter()
+                .filter(|f| (*f).file_hash() == conf.font_hash)
+                .collect();
+            glyph_cache.insert(conf, font.first().unwrap().rasterize_config(conf));
+            glyph_cache.get(&conf).unwrap().clone()
         }
     }
 
@@ -49,34 +72,40 @@ impl Font {
         x_offset: u32,
         y_offset: u32,
     ) -> (u32, u32) {
-        let mut width = 0.;
-        for letter in text.chars() {
-            let (meta, bitmap) = self.render_glyph(GlyphRasterConfig {
-                glyph_index: self.font.lookup_glyph_index(letter) as u16,
-                px: self.scale,
-                font_index: 0,
-            });
+        let mut width = 0;
+        let mut layout = self.layout.borrow_mut();
+        layout.reset(&LayoutSettings::default());
+        for c in text.chars() {
+            let mut font_index = 0;
+            for (i, font) in self.fonts.iter().enumerate() {
+                if font.lookup_glyph_index(c) != 0 {
+                    font_index = i;
+                    break;
+                }
+            }
+            layout.append(
+                &self.fonts,
+                &TextStyle::new(&c.to_string(), self.scale, font_index),
+            );
+        }
+
+        for glyph in layout.glyphs() {
+            let (_, bitmap) = self.render_glyph(glyph.key);
             for (i, alpha) in bitmap.iter().enumerate() {
                 if alpha != &0 {
-                    let x = ((i % meta.width) as f32 + width + x_offset as f32 + meta.xmin as f32)
-                        as u32;
-                    let y = ((i as f32 / meta.width as f32) + y_offset as f32 + self.scale
-                        - meta.height as f32
-                        - meta.ymin as f32) as u32;
-                    if x < image.width() && y < image.height() {
-                        if alpha == &255 {
-                            image.put_pixel(x, y, image::Rgba([color.0, color.1, color.2, 255]));
-                        } else {
-                            image
-                                .get_pixel_mut(x, y)
-                                .blend(&image::Rgba([color.0, color.1, color.2, *alpha]));
-                        }
-                    }
+                    let x = glyph.x + x_offset as f32 + (i % glyph.width) as f32;
+                    let y = glyph.y + y_offset as f32 + (i / glyph.width) as f32;
+
+                    image
+                        .get_pixel_mut(x as u32, y as u32)
+                        .blend(&image::Rgba([color.0, color.1, color.2, *alpha]));
                 }
             }
-            width += meta.advance_width;
+        }
+        if let Some(glyph) = layout.glyphs().last() {
+            width = glyph.x as usize + glyph.width;
         }
 
-        (width as u32, self.scale as u32)
+        (width as u32, layout.height() as u32)
     }
 }
diff --git a/src/keybinds.rs b/src/keybinds.rs
index 277a2b7..7a42788 100644
--- a/src/keybinds.rs
+++ b/src/keybinds.rs
@@ -7,26 +7,26 @@ use std::fmt;
 use std::hash::{Hash, Hasher};
 use x11_keysymdef::lookup_by_name;
 
-use crate::config::Config;
+use crate::config::KeybindingsConfig;
 
 pub struct Keybindings {
     inner: HashMap<KeyCombo, Action>,
 }
 
-impl From<Config> for Keybindings {
-    fn from(config: Config) -> Self {
+impl From<KeybindingsConfig> for Keybindings {
+    fn from(config: KeybindingsConfig) -> Self {
         let mut res = Keybindings {
             inner: HashMap::new(),
         };
 
-        res.add_key_combos(Action::Complete, &config.keybindings.complete);
-        res.add_key_combos(Action::Execute, &config.keybindings.execute);
-        res.add_key_combos(Action::Exit, &config.keybindings.exit);
-        res.add_key_combos(Action::Delete, &config.keybindings.delete);
-        res.add_key_combos(Action::DeleteWord, &config.keybindings.delete_word);
-        res.add_key_combos(Action::NavUp, &config.keybindings.nav_up);
-        res.add_key_combos(Action::NavDown, &config.keybindings.nav_down);
-        res.add_key_combos(Action::Paste, &config.keybindings.paste);
+        res.add_key_combos(Action::Complete, &config.complete);
+        res.add_key_combos(Action::Execute, &config.execute);
+        res.add_key_combos(Action::Exit, &config.exit);
+        res.add_key_combos(Action::Delete, &config.delete);
+        res.add_key_combos(Action::DeleteWord, &config.delete_word);
+        res.add_key_combos(Action::NavUp, &config.nav_up);
+        res.add_key_combos(Action::NavDown, &config.nav_down);
+        res.add_key_combos(Action::Paste, &config.paste);
 
         res
     }
diff --git a/src/main.rs b/src/main.rs
index d72b420..fa6edf3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -20,11 +20,11 @@ use nix::{
     sys::wait::{waitpid, WaitPidFlag, WaitStatus},
     unistd::{fork, ForkResult},
 };
+use notify_rust::Notification;
 use simplelog::{ColorChoice, Config as LogConfig, LevelFilter, TermLogger, TerminalMode};
 use std::error::Error;
 use std::time::Duration;
 use tokio::task::JoinHandle;
-use notify_rust::Notification;
 
 mod color;
 mod config;
@@ -74,10 +74,12 @@ async fn run() -> Result<Option<JoinHandle<()>>, Box<dyn Error>> {
         let decrease_interval = config.history.decrease_interval;
         tokio::spawn(async move { history::get_history(decrease_interval) })
     };
-    let font_handle = {
-        let font = config.font.clone();
-        let font_size = config.font_size;
-        tokio::spawn(async move { font::Font::new(&font, font_size) })
+    let font = if let Some(font_name) = config.font {
+        let mut font_names = config.fonts.clone();
+        font_names.insert(0, font_name);
+        font::Font::new(font_names, config.font_size)
+    } else {
+        font::Font::new(config.fonts, config.font_size)
     };
 
     let (env, display, queue) =
@@ -115,11 +117,10 @@ async fn run() -> Result<Option<JoinHandle<()>>, Box<dyn Error>> {
 
     let mut matched_exe: Vec<&String> = applications.iter().collect();
     let mut need_redraw = false;
-    let mut data = DData::new(&display, config.clone().into());
+    let mut data = DData::new(&display, config.keybindings.clone().into());
     let mut selection = 0;
     let mut select_query = false;
-
-    let mut font = font_handle.await?;
+    let mut font = font.await?;
 
     loop {
         let gui::DData { query, action, .. } = &mut data;


Test patch: