v1.2.1: text measuring support

several malvim features added, font data caching, taskbar title overflow fix, new background
This commit is contained in:
stjet
2025-09-06 06:23:57 +00:00
parent 08c2358bdc
commit 10daa9982b
12 changed files with 219 additions and 80 deletions

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ming-wm" name = "ming-wm"
version = "1.2.0" version = "1.2.1"
repository = "https://github.com/stjet/ming-wm" repository = "https://github.com/stjet/ming-wm"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
edition = "2021" edition = "2021"

BIN
bmps/arhants1440x842.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

View File

@@ -14,22 +14,28 @@ It is probably best to read a Vim tutorial for the basics. All supportd keystrok
- `w[rite]` - `w[rite]`
- `/<query>` - `/<query>`
Tab completion is supported for the `<file>` argument. Tab completion is supported for the `<file>` argument. Down arrow will clear the current command, and up arrow will fill in the last ran command.
### Supported in Normal Mode ### Supported in Normal Mode
- `:` - `:`
- `i` - `i`
- `o`, `O`
- `A` - `A`
- `r` - `r`
- `dd` - `dd`
- `dw` - `<number>dd`
- `dw` (`dw` is not identical to vim's behaviour), `dW`
- `d$` - `d$`
- `G` - `G`
- `gg` - `gg`
- `<number>gg` - `<number>gg`
- `f<char>` - `f<char>`, `F<char>`
- `F<char>` - `<number>f<char>`, `<number>F<char>`
- `;` (same as `f<char>` but with the char the cursor is on, so not the same as vim)
- `<num>;`
- `,` (same as `F<char>` but with the char the cursor is on, so not the same as vim)
- `<num>,`
- `x` - `x`
- `h` (or left arrow), `j` (or down arrow), `k` (or up arrow), `l` (or right arrow) - `h` (or left arrow), `j` (or down arrow), `k` (or up arrow), `l` (or right arrow)
- `<num>h`, `<num>j` (or down arrow), `<num>k` (or up arrow), `<num>l` - `<num>h`, `<num>j` (or down arrow), `<num>k` (or up arrow), `<num>l`

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ming-wm-lib" name = "ming-wm-lib"
version = "0.2.1" version = "0.2.2"
repository = "https://github.com/stjet/ming-wm" repository = "https://github.com/stjet/ming-wm"
description = "library for building windows for ming-wm in rust" description = "library for building windows for ming-wm in rust"
readme = "README.md" readme = "README.md"

107
ming-wm-lib/src/fonts.rs Normal file
View File

@@ -0,0 +1,107 @@
use std::fs::File;
use std::io::Read;
use std::collections::HashMap;
use crate::dirs;
#[derive(Clone)]
pub struct FontCharInfo {
pub c: char,
pub data: Vec<Vec<u8>>,
pub top_offset: u8,
pub height: usize,
pub width: usize,
}
fn get_font_char(dir: &str, c: char) -> Option<FontCharInfo> {
let c = if c == '/' { '𐘋' } else if c == '\\' { '𐚆' } else if c == '.' { '𐘅' } else { c };
if let Ok(mut file) = File::open(dir.to_string() + "/" + &c.to_string() + ".alpha") {
let mut ch: Vec<Vec<u8>> = Vec::new();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let lines: Vec<&str> = contents.split("\n").collect();
for ln in 1..lines.len() {
//.unwrap_or(0) is important because zeroes are just empty
ch.push(lines[ln].replace(":", ",,,,").replace(";", ",,,").replace(".", ",,").split(",").map(|n| n.parse().unwrap_or(0)).collect());
}
return Some(FontCharInfo {
c,
top_offset: lines[0].parse().unwrap(),
height: lines.len() - 1,
width: ch[0].len(),
data: ch,
});
}
None
}
pub fn get_font_char_from_fonts(fonts: &[String], c: char) -> FontCharInfo {
for font in fonts {
let p = dirs::exe_dir(Some(&("ming_bmps/".to_string() + &font))).to_string_lossy().to_string();
if let Some(font_char) = get_font_char(&p, c) {
return font_char;
}
}
let p = dirs::exe_dir(Some(&("ming_bmps/".to_string() + &fonts[0]))).to_string_lossy().to_string();
//so a ? char should be in every font. otherwise will just return blank
get_font_char(&p, '?').unwrap_or(FontCharInfo {
c: '?',
data: vec![vec![0]],
top_offset: 0,
height: 1,
width: 1,
})
}
pub struct MeasureInfo {
pub height: usize,
pub width: usize,
}
/// Doesn't take into account `horiz_spacing`, which defaults to 1 per character
pub fn measure_text(fonts: &[String], text: String) -> MeasureInfo {
let mut height = 0;
let mut width = 0;
for c in text.chars() {
let i = get_font_char_from_fonts(fonts, c);
let c_height = i.top_offset as usize + i.height;
if c_height > height {
height = c_height;
}
width += i.width;
}
MeasureInfo {
height,
width,
}
}
#[derive(Default)]
pub struct CachedFontCharGetter {
cache: HashMap<char, FontCharInfo>,
cache_size: usize, //# of items cached
pub max_cache_size: usize,
}
impl CachedFontCharGetter {
pub fn new(max_cache_size: usize) -> Self {
let mut s: Self = Default::default();
s.max_cache_size = max_cache_size;
s
}
pub fn get(&mut self, fonts: &[String], c: char) -> FontCharInfo {
if let Some(cached) = self.cache.get(&c) {
cached.clone()
} else {
let got = get_font_char_from_fonts(fonts, c);
if self.cache_size == self.max_cache_size {
self.cache_size = 0;
self.cache = HashMap::new();
}
self.cache.insert(c, got.clone());
self.cache_size += 1;
got
}
}
}

View File

@@ -5,6 +5,7 @@ pub mod serialize;
pub mod messages; pub mod messages;
pub mod ipc; pub mod ipc;
pub mod components; pub mod components;
pub mod fonts;
pub mod dirs; pub mod dirs;
pub mod utils; pub mod utils;
pub mod logging; pub mod logging;

View File

@@ -223,3 +223,4 @@ pub fn get_all_files(dir: PathBuf) -> Vec<PathBuf> {
} }
files files
} }

View File

@@ -17,6 +17,7 @@ use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLik
use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse, WindowManagerRequest, ShortcutType }; use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse, WindowManagerRequest, ShortcutType };
use ming_wm_lib::framebuffer_types::Dimensions; use ming_wm_lib::framebuffer_types::Dimensions;
use ming_wm_lib::themes::ThemeInfo; use ming_wm_lib::themes::ThemeInfo;
use ming_wm_lib::fonts::measure_text;
use ming_wm_lib::utils::{ concat_paths, random_u32, get_all_files, path_autocomplete, format_seconds, Substring }; use ming_wm_lib::utils::{ concat_paths, random_u32, get_all_files, path_autocomplete, format_seconds, Substring };
use ming_wm_lib::dirs::home; use ming_wm_lib::dirs::home;
use ming_wm_lib::ipc::listen; use ming_wm_lib::ipc::listen;
@@ -150,11 +151,15 @@ impl WindowLike for AudioPlayer {
let queue = &internal_locked.queue; let queue = &internal_locked.queue;
let current = &queue[queue.len() - sink_len]; let current = &queue[queue.len() - sink_len];
let current_name = current.0.file_name().unwrap().to_string_lossy().into_owned(); let current_name = current.0.file_name().unwrap().to_string_lossy().into_owned();
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - current_name.len() * MONO_WIDTH as usize / 2, 2], vec!["nimbus-romono".to_string(), "shippori-mincho".to_string()], current_name.clone(), theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH))); let fonts = ["nimbus-roman".to_string(), "shippori-mincho".to_string()];
let cn_width = measure_text(&fonts, current_name.clone()).width;
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - cn_width / 2, 2], fonts.to_vec(), current_name.clone(), theme_info.text, theme_info.background, Some(0), None));
if let Some(artist) = &current.2 { if let Some(artist) = &current.2 {
let artist_string = "by ".to_string() + &artist; let artist_string = "by ".to_string() + &artist;
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - artist_string.len() * MONO_WIDTH as usize / 2, LINE_HEIGHT + 2], vec!["nimbus-romono".to_string()], artist_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH))); let as_width = measure_text(&fonts, artist_string.clone()).width;
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - as_width / 2, LINE_HEIGHT + 2], fonts.to_vec(), artist_string, theme_info.text, theme_info.background, Some(0), None));
} }
//in this case no chance of mincho so MONO_WIDTH method of calculating width is ok
let time_string = format!("{}/{}", format_seconds(internal_locked.sink.get_pos().as_secs()), format_seconds(current.1)); let time_string = format!("{}/{}", format_seconds(internal_locked.sink.get_pos().as_secs()), format_seconds(current.1));
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - time_string.len() * MONO_WIDTH as usize / 2, LINE_HEIGHT * 2 + 2], vec!["nimbus-romono".to_string()], time_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH))); instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - time_string.len() * MONO_WIDTH as usize / 2, LINE_HEIGHT * 2 + 2], vec!["nimbus-romono".to_string()], time_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH)));
} else { } else {

View File

@@ -19,6 +19,8 @@ const LINE_HEIGHT: usize = 18;
const PADDING: usize = 2; const PADDING: usize = 2;
const BAND_HEIGHT: usize = 19; const BAND_HEIGHT: usize = 19;
const WORD_END: [char; 8] = ['.', ',', ':', '[', ']', '{', '}', ' '];
struct FileInfo { struct FileInfo {
pub name: String, pub name: String,
pub path: String, pub path: String,
@@ -43,6 +45,12 @@ enum State {
// //
} }
impl State {
fn is_numberable(&self) -> bool {
*self == State::Maybeg || *self == State::Find || *self == State::BackFind
}
}
#[derive(Default, PartialEq)] #[derive(Default, PartialEq)]
enum Mode { enum Mode {
#[default] #[default]
@@ -77,6 +85,7 @@ struct Malvim {
state: State, state: State,
mode: Mode, mode: Mode,
command: Option<String>, command: Option<String>,
prev_command: Option<String>,
bottom_message: Option<String>, bottom_message: Option<String>,
maybe_num: Option<usize>, maybe_num: Option<usize>,
files: Vec<FileInfo>, files: Vec<FileInfo>,
@@ -178,7 +187,7 @@ impl WindowLike for Malvim {
} }
let new_length = current_file.content[current_file.line_pos].chars().count(); let new_length = current_file.content[current_file.line_pos].chars().count();
current_file.cursor_pos = Malvim::calc_new_cursor_pos(current_file.cursor_pos, new_length); current_file.cursor_pos = Malvim::calc_new_cursor_pos(current_file.cursor_pos, new_length);
} else if key_press.key == 'w' || key_press.key == '$' { } else if key_press.key == 'w' || key_press.key == 'W' || key_press.key == '$' {
let line = &current_file.content[current_file.line_pos]; let line = &current_file.content[current_file.line_pos];
let line_len = line.chars().count(); let line_len = line.chars().count();
if line_len > 0 && current_file.cursor_pos < line_len { if line_len > 0 && current_file.cursor_pos < line_len {
@@ -186,11 +195,19 @@ impl WindowLike for Malvim {
let mut line_chars = line.chars().skip(current_file.cursor_pos).peekable(); let mut line_chars = line.chars().skip(current_file.cursor_pos).peekable();
//deref is Copy //deref is Copy
let current_char = *line_chars.peek().unwrap(); let current_char = *line_chars.peek().unwrap();
let offset = if key_press.key == 'w' { let offset = if key_press.key == 'W' || key_press.key == 'w' {
line_chars.position(|c| if current_char == ' ' { line_chars.position(|c| if key_press.key == 'w' {
if WORD_END.contains(&current_char) {
c != current_char
} else {
WORD_END.contains(&c)
}
} else {
if current_char == ' ' {
c != ' ' c != ' '
} else { } else {
c == ' ' c == ' '
}
}).unwrap_or(line_len - current_file.cursor_pos) }).unwrap_or(line_len - current_file.cursor_pos)
} else { } else {
line_chars.count() line_chars.count()
@@ -212,11 +229,17 @@ impl WindowLike for Malvim {
} }
changed = false; changed = false;
self.state = State::None; self.state = State::None;
} else if self.state == State::Find || self.state == State::BackFind { } else if self.state == State::Find || self.state == State::BackFind || key_press.key == ';' || key_press.key == ',' {
let old_pos = current_file.cursor_pos; let mut old_pos = current_file.cursor_pos;
let find_pos = if self.state == State::Find { let find_char = if self.state == State::Find || self.state == State::BackFind {
key_press.key
} else {
current_file.content[current_file.line_pos].chars().nth(old_pos).unwrap()
};
for _ in 0..self.maybe_num.unwrap_or(1) {
let find_pos = if self.state == State::Find || key_press.key == ';' {
if old_pos < current_file.content[current_file.line_pos].chars().count() { if old_pos < current_file.content[current_file.line_pos].chars().count() {
let found_index = current_file.content[current_file.line_pos].chars().skip(old_pos + 1).position(|c| c == key_press.key); let found_index = current_file.content[current_file.line_pos].chars().skip(old_pos + 1).position(|c| c == find_char);
if let Some(found_index) = found_index { if let Some(found_index) = found_index {
old_pos + found_index + 1 old_pos + found_index + 1
} else { } else {
@@ -228,7 +251,7 @@ impl WindowLike for Malvim {
} else { } else {
//how does this work again? no idea //how does this work again? no idea
if old_pos != 0 { if old_pos != 0 {
let found_index = current_file.content[current_file.line_pos].chars().rev().skip(current_length - old_pos).position(|c| c == key_press.key); let found_index = current_file.content[current_file.line_pos].chars().rev().skip(current_length - old_pos).position(|c| c == find_char);
if let Some(found_index) = found_index { if let Some(found_index) = found_index {
old_pos - found_index - 1 old_pos - found_index - 1
} else { } else {
@@ -239,6 +262,8 @@ impl WindowLike for Malvim {
} }
}; };
current_file.cursor_pos = find_pos; current_file.cursor_pos = find_pos;
old_pos = current_file.cursor_pos;
}
changed = false; changed = false;
self.state = State::None; self.state = State::None;
} else if key_press.key == 'x' { } else if key_press.key == 'x' {
@@ -394,7 +419,7 @@ impl WindowLike for Malvim {
changed = false; changed = false;
} }
//reset maybe_num if not num //reset maybe_num if not num
if !numbered && self.state != State::Maybeg && self.state != State::MaybeDelete { if !numbered && !self.state.is_numberable() {
self.maybe_num = None; self.maybe_num = None;
} }
} else if self.mode == Mode::Command { } else if self.mode == Mode::Command {
@@ -402,7 +427,8 @@ impl WindowLike for Malvim {
let command = self.command.clone().unwrap_or("".to_string()); let command = self.command.clone().unwrap_or("".to_string());
if key_press.is_enter() { if key_press.is_enter() {
new = self.process_command(); new = self.process_command();
self.command = None; self.prev_command = self.command.take();
//line above does same as `self.command = None`
self.mode = Mode::Normal; self.mode = Mode::Normal;
} else if key_press.key == '\t' { //tab } else if key_press.key == '\t' { //tab
let mut parts = command.split(" ").skip(1); let mut parts = command.split(" ").skip(1);
@@ -425,6 +451,12 @@ impl WindowLike for Malvim {
if command.len() > 0 { if command.len() > 0 {
self.command = Some(command.remove_last()); self.command = Some(command.remove_last());
} }
} else if key_press.is_arrow() {
if key_press.is_up_arrow() {
self.command = self.prev_command.clone();
} else if key_press.is_down_arrow() {
self.command = Some(String::new());
}
} else { } else {
self.command = Some(command.to_string() + &key_press.key.to_string()); self.command = Some(command.to_string() + &key_press.key.to_string());
} }

View File

@@ -6,6 +6,7 @@ use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLik
use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse, WindowManagerRequest, ShortcutType, InfoType, WindowsVec }; use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse, WindowManagerRequest, ShortcutType, InfoType, WindowsVec };
use ming_wm_lib::framebuffer_types::Dimensions; use ming_wm_lib::framebuffer_types::Dimensions;
use ming_wm_lib::themes::ThemeInfo; use ming_wm_lib::themes::ThemeInfo;
use ming_wm_lib::fonts::measure_text;
use ming_wm_lib::components::Component; use ming_wm_lib::components::Component;
use ming_wm_lib::components::toggle_button::ToggleButton; use ming_wm_lib::components::toggle_button::ToggleButton;
@@ -78,7 +79,25 @@ impl WindowLike for Taskbar {
break; break;
} }
let info = &self.windows_in_workspace[wi]; let info = &self.windows_in_workspace[wi];
let name = &info.1; let max_text_width = META_WIDTH - PADDING * 2;
//horiz_spacing is by default 1 per char, which measure_text doesn't take into account
let to_measure = info.1.clone();
let to_measure_len = to_measure.chars().count();
let name = if measure_text(&["nimbus-roman".to_string()], to_measure).width + to_measure_len > max_text_width {
let mut current = String::new();
for c in info.1.chars() {
//horiz_spacing is 1 by default
let to_measure = current.clone() + &c.to_string() + "...";
let to_measure_len = to_measure.chars().count();
if measure_text(&["nimbus-roman".to_string()], to_measure).width + to_measure_len > max_text_width {
break;
}
current += &c.to_string();
}
current + "..."
} else {
info.1.clone()
};
let mut b = ToggleButton::new(name.to_string() + "-window", [PADDING * 2 + 44 + (META_WIDTH + PADDING) * wi, PADDING], [META_WIDTH, self.dimensions[1] - (PADDING * 2)], name.to_string(), TaskbarMessage::Nothing, TaskbarMessage::Nothing); let mut b = ToggleButton::new(name.to_string() + "-window", [PADDING * 2 + 44 + (META_WIDTH + PADDING) * wi, PADDING], [META_WIDTH, self.dimensions[1] - (PADDING * 2)], name.to_string(), TaskbarMessage::Nothing, TaskbarMessage::Nothing);
b.inverted = info.0 == self.focused_id; b.inverted = info.0 == self.focused_id;
instructions.extend(b.draw(theme_info)); instructions.extend(b.draw(theme_info));
@@ -123,5 +142,3 @@ impl Taskbar {
} }
} }
} }

View File

@@ -4,9 +4,7 @@ use std::vec::Vec;
use bmp_rust::bmp::BMP; use bmp_rust::bmp::BMP;
use ming_wm_lib::framebuffer_types::*; use ming_wm_lib::framebuffer_types::*;
use crate::fs::get_font_char_from_fonts; use ming_wm_lib::fonts::{ CachedFontCharGetter, FontCharInfo };
type FontChar = (char, Vec<Vec<u8>>, u8);
fn color_with_alpha(color: RGBColor, bg_color: RGBColor, alpha: u8) -> RGBColor { fn color_with_alpha(color: RGBColor, bg_color: RGBColor, alpha: u8) -> RGBColor {
/*let factor: f32 = alpha as f32 / 255.0; /*let factor: f32 = alpha as f32 / 255.0;
@@ -47,6 +45,7 @@ pub struct FramebufferInfo {
//currently doesn't check if writing onto next line accidentally //currently doesn't check if writing onto next line accidentally
pub struct FramebufferWriter { pub struct FramebufferWriter {
info: FramebufferInfo, info: FramebufferInfo,
fc_getter: CachedFontCharGetter,
buffer: Vec<u8>, buffer: Vec<u8>,
saved_buffer: Option<Vec<u8>>, saved_buffer: Option<Vec<u8>>,
rotate_buffer: Option<Vec<u8>>, rotate_buffer: Option<Vec<u8>>,
@@ -57,6 +56,7 @@ impl FramebufferWriter {
pub fn new(grayscale: bool) -> Self { pub fn new(grayscale: bool) -> Self {
Self { Self {
info: Default::default(), info: Default::default(),
fc_getter: CachedFontCharGetter::new(128), //an arbitrary high-ish number for max cache size
buffer: Vec::new(), buffer: Vec::new(),
saved_buffer: None, saved_buffer: None,
rotate_buffer: None, rotate_buffer: None,
@@ -128,12 +128,11 @@ impl FramebufferWriter {
} }
} }
pub fn draw_char(&mut self, top_left: Point, char_info: &FontChar, color: RGBColor, bg_color: RGBColor) { pub fn draw_char(&mut self, top_left: Point, char_info: &FontCharInfo, color: RGBColor, bg_color: RGBColor) {
let mut start_pos; let mut start_pos;
for row in 0..char_info.1.len() { for row in 0..char_info.height {
//char_info.2 is vertical offset start_pos = ((top_left[1] + row + char_info.top_offset as usize) * self.info.stride + top_left[0]) * self.info.bytes_per_pixel;
start_pos = ((top_left[1] + row + char_info.2 as usize) * self.info.stride + top_left[0]) * self.info.bytes_per_pixel; for col in &char_info.data[row] {
for col in &char_info.1[row] {
if col > &0 { if col > &0 {
if start_pos + 3 < self.info.byte_len { if start_pos + 3 < self.info.byte_len {
self._draw_pixel(start_pos, color_with_alpha(color, bg_color, *col)); self._draw_pixel(start_pos, color_with_alpha(color, bg_color, *col));
@@ -226,8 +225,8 @@ impl FramebufferWriter {
if c == ' ' { if c == ' ' {
top_left[0] += mono_width.unwrap_or(5) as usize; top_left[0] += mono_width.unwrap_or(5) as usize;
} else { } else {
let char_info = get_font_char_from_fonts(&fonts, c); let char_info = self.fc_getter.get(&fonts, c);
let char_width = char_info.1[0].len(); let char_width = char_info.width;
let add_after: usize; let add_after: usize;
if let Some(mono_width) = mono_width { if let Some(mono_width) = mono_width {
let mono_width = mono_width as usize; let mono_width = mono_width as usize;

View File

@@ -1,38 +1,9 @@
use std::fs::{ read_dir, File }; use std::fs::read_dir;
use std::io::Read;
use std::collections::HashMap; use std::collections::HashMap;
use ming_wm_lib::dirs; use ming_wm_lib::dirs;
use ming_wm_lib::utils::get_rest_of_split; use ming_wm_lib::utils::get_rest_of_split;
fn get_font_char(dir: &str, c: char) -> Option<(char, Vec<Vec<u8>>, u8)> {
let c = if c == '/' { '𐘋' } else if c == '\\' { '𐚆' } else if c == '.' { '𐘅' } else { c };
if let Ok(mut file) = File::open(dir.to_string() + "/" + &c.to_string() + ".alpha") {
let mut ch: Vec<Vec<u8>> = Vec::new();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let lines: Vec<&str> = contents.split("\n").collect();
for ln in 1..lines.len() {
//.unwrap_or(0) is important because zeroes are just empty
ch.push(lines[ln].replace(":", ",,,,").replace(";", ",,,").replace(".", ",,").split(",").map(|n| n.parse().unwrap_or(0)).collect());
}
return Some((c, ch, lines[0].parse().unwrap()));
}
None
}
pub fn get_font_char_from_fonts(fonts: &[String], c: char) -> (char, Vec<Vec<u8>>, u8) {
for font in fonts {
let p = dirs::exe_dir(Some(&("ming_bmps/".to_string() + &font))).to_string_lossy().to_string();
if let Some(font_char) = get_font_char(&p, c) {
return font_char;
}
}
let p = dirs::exe_dir(Some(&("ming_bmps/".to_string() + &fonts[0]))).to_string_lossy().to_string();
//so a ? char should be in every font. otherwise will just return blank
get_font_char(&p, '?').unwrap_or(('?', vec![vec![0]], 0))
}
//Category, Vec<Display name, file name> //Category, Vec<Display name, file name>
pub type ExeWindowInfos = HashMap<String, Vec<(String, String)>>; pub type ExeWindowInfos = HashMap<String, Vec<(String, String)>>;