Configurable font stretch, font weight, and bold font weight

* Store a font name => font faces map

 * Only list font names that have `NORMAL` and `BOLD` faces with
   `Normal` stretch. This will give us defaults and fall-backs that are
   guaranteed to always exist.

 * Filter by stretch first, with `Normal` chosen as the always existing
   default.

 * Then only list font weights supported by the font name stretch
   selected, for the normal and bold cases.

 * When changing the font name selected, the stretch/weight options
   will stay the same if supported by the new font name, or revert to
   the always existing fall-backs.

Signed-off-by: Mohammad AlSaleh <CE.Mohammad.AlSaleh@gmail.com>
This commit is contained in:
Mohammad AlSaleh 2024-01-10 01:19:04 +03:00 committed by Jeremy Soller
parent c74d5d6f56
commit 5eba6eb4d6
6 changed files with 251 additions and 12 deletions

1
Cargo.lock generated
View file

@ -874,6 +874,7 @@ dependencies = [
"lazy_static",
"libcosmic",
"log",
"paste",
"rust-embed",
"serde",
"tokio",

View file

@ -16,6 +16,7 @@ tokio = { version = "1", features = ["sync"] }
i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.6"
rust-embed = "6"
paste = "1.0"
[dependencies.cosmic-text]
git = "https://github.com/pop-os/cosmic-text.git"

View file

@ -12,6 +12,9 @@ light = Light
syntax-dark = Syntax dark
syntax-light = Syntax light
default-font = Default font
default-font-stretch = Default font stretch
default-font-weight = Default font weight
default-bold-font-weight = Default bold font weight
default-font-size = Default font size
default-zoom-step = Default zoom step

View file

@ -4,9 +4,12 @@ use cosmic::{
cosmic_config::{self, cosmic_config_derive::CosmicConfigEntry, CosmicConfigEntry},
theme,
};
use cosmic_text::Metrics;
use cosmic_text::{Metrics, Weight, Stretch};
use serde::{Deserialize, Serialize};
use std::sync::OnceLock;
use std::collections::BTreeMap;
pub const CONFIG_VERSION: u64 = 1;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
@ -31,6 +34,9 @@ pub struct Config {
pub app_theme: AppTheme,
pub font_name: String,
pub font_size: u16,
pub font_weight: u16,
pub bold_font_weight: u16,
pub font_stretch: u16,
pub font_size_zoom_step_mul_100: u16,
pub show_headerbar: bool,
pub syntax_theme_dark: String,
@ -43,6 +49,9 @@ impl Default for Config {
app_theme: AppTheme::System,
font_name: "Fira Mono".to_string(),
font_size: 14,
font_weight: Weight::NORMAL.0,
bold_font_weight: Weight::BOLD.0,
font_stretch: Stretch::Normal.to_number(),
font_size_zoom_step_mul_100: 100,
show_headerbar: true,
syntax_theme_dark: "COSMIC Dark".to_string(),
@ -75,4 +84,23 @@ impl Config {
&self.syntax_theme_light
}
}
pub fn typed_font_stretch(&self) -> Stretch {
macro_rules! populate_num_typed_map {
($($stretch:ident,)+) => {
let mut map = BTreeMap::new();
$(map.insert(Stretch::$stretch.to_number(), Stretch::$stretch);)+
map
};
}
static NUM_TO_TYPED_MAP: OnceLock<BTreeMap<u16, Stretch>> = OnceLock::new();
NUM_TO_TYPED_MAP.get_or_init(|| {
populate_num_typed_map!{
UltraCondensed, ExtraCondensed, Condensed, SemiCondensed,
Normal, SemiExpanded, Expanded, ExtraExpanded, UltraExpanded,
}
})[&self.font_stretch]
}
}

View file

@ -20,8 +20,8 @@ use cosmic::{
widget::{self, segmented_button},
Application, ApplicationExt, Element,
};
use cosmic_text::Family;
use std::{any::TypeId, collections::HashMap, env, process, sync::Mutex};
use cosmic_text::{Family, Weight, Stretch, fontdb::FaceInfo};
use std::{any::TypeId, collections::{HashMap, BTreeMap, BTreeSet}, env, process, sync::Mutex};
use tokio::sync::mpsc;
use config::{AppTheme, Config, CONFIG_VERSION};
@ -140,6 +140,9 @@ pub enum Message {
Copy(Option<segmented_button::Entity>),
DefaultFont(usize),
DefaultFontSize(usize),
DefaultFontStretch(usize),
DefaultFontWeight(usize),
DefaultBoldFontWeight(usize),
DefaultZoomStep(usize),
Paste(Option<segmented_button::Entity>),
PasteValue(Option<segmented_button::Entity>, String),
@ -186,6 +189,13 @@ pub struct App {
font_names: Vec<String>,
font_size_names: Vec<String>,
font_sizes: Vec<u16>,
font_name_faces_map: BTreeMap<String, Vec<FaceInfo>>,
all_font_weights_vals_names_map: BTreeMap<u16, String>,
all_font_stretches_vals_names_map: BTreeMap<Stretch, String>,
curr_font_weight_names: Vec<String>,
curr_font_weights: Vec<u16>,
curr_font_stretch_names: Vec<String>,
curr_font_stretches: Vec<Stretch>,
zoom_adj: i8,
zoom_step_names: Vec<String>,
zoom_steps: Vec<u16>,
@ -236,6 +246,57 @@ impl App {
self.set_window_title(window_title)
}
fn set_curr_font_weights_and_stretches(&mut self) {
let curr_font_faces = &self.font_name_faces_map[&self.config.font_name];
self.curr_font_stretches = curr_font_faces
.iter()
.map(|face| face.stretch)
.collect::<BTreeSet<_>>() // remove duplicates and sort
.into_iter()
.collect();
self.curr_font_stretch_names = self.curr_font_stretches.iter()
.map(|stretch| &self.all_font_stretches_vals_names_map[stretch])
.cloned()
.collect::<Vec<_>>();
if !self.curr_font_stretches.contains(&self.config.typed_font_stretch()) {
self.config.font_stretch = Stretch::Normal.to_number();
}
let curr_weights = |conf_stretch| curr_font_faces
.iter()
.filter(|face| face.stretch == conf_stretch)
.map(|face| face.weight.0)
.collect::<BTreeSet<_>>() // remove duplicates and sort
.into_iter()
.collect();
self.curr_font_weights = curr_weights(self.config.typed_font_stretch());
if self.curr_font_weights.is_empty() {
// stretch fallback
self.config.font_stretch = Stretch::Normal.to_number();
}
self.curr_font_weights = curr_weights(self.config.typed_font_stretch());
assert!(!self.curr_font_weights.is_empty());
self.curr_font_weight_names = self.curr_font_weights.iter()
.map(|weight| &self.all_font_weights_vals_names_map[weight])
.cloned()
.collect::<Vec<_>>();
if !self.curr_font_weights.contains(&self.config.font_weight) {
self.config.font_weight = Weight::NORMAL.0;
}
if !self.curr_font_weights.contains(&self.config.bold_font_weight) {
self.config.bold_font_weight = Weight::BOLD.0;
}
}
fn settings(&self) -> Element<Message> {
let app_theme_selected = match self.config.app_theme {
AppTheme::Dark => 1,
@ -261,6 +322,18 @@ impl App {
.font_sizes
.iter()
.position(|font_size| font_size == &self.config.font_size);
let font_stretch_selected = self
.curr_font_stretches
.iter()
.position(|font_stretch| font_stretch == &self.config.typed_font_stretch());
let font_weight_selected = self
.curr_font_weights
.iter()
.position(|font_weight| font_weight == &self.config.font_weight);
let bold_font_weight_selected = self
.curr_font_weights
.iter()
.position(|font_weight| font_weight == &self.config.bold_font_weight);
let zoom_step_selected = self
.zoom_steps
.iter()
@ -314,6 +387,27 @@ impl App {
}),
),
)
.add(
widget::settings::item::builder(fl!("default-font-stretch")).control(
widget::dropdown(&self.curr_font_stretch_names, font_stretch_selected, |index| {
Message::DefaultFontStretch(index)
}),
),
)
.add(
widget::settings::item::builder(fl!("default-font-weight")).control(
widget::dropdown(&self.curr_font_weight_names, font_weight_selected, |index| {
Message::DefaultFontWeight(index)
}),
),
)
.add(
widget::settings::item::builder(fl!("default-bold-font-weight")).control(
widget::dropdown(&self.curr_font_weight_names, bold_font_weight_selected, |index| {
Message::DefaultBoldFontWeight(index)
}),
),
)
.add(
widget::settings::item::builder(fl!("show-headerbar"))
.toggler(self.config.show_headerbar, Message::ShowHeaderBar),
@ -361,24 +455,34 @@ impl Application for App {
let app_themes = vec![fl!("match-desktop"), fl!("dark"), fl!("light")];
let font_names = {
let mut font_names = Vec::new();
let font_name_faces_map = {
let mut font_name_faces_map = BTreeMap::<_, Vec<_>>::new();
let mut font_system = font_system().write().unwrap();
//TODO: do not repeat, used in Tab::new
let attrs = cosmic_text::Attrs::new().family(Family::Monospace);
for face in font_system.raw().db().faces() {
if attrs.matches(face) && face.monospaced {
// only monospace fonts and weights that match named constants.
let weight = face.weight.0;
if face.monospaced && {1..9}.contains(&{weight / 100}) && weight % 100 == 0 {
//TODO: get localized name if possible
let font_name = face
.families
.get(0)
.map_or_else(|| face.post_script_name.to_string(), |x| x.0.to_string());
font_names.push(font_name);
font_name_faces_map.entry(font_name).or_default().push(face.clone());
}
}
font_names.sort();
font_names
// only keep fonts that have both NORMAL and BOLD weights with both having
// a `Stretch::Normal` face.
// This is important for fallbacks.
font_name_faces_map.retain(|_, v| {
let has_normal = v.iter().any(|face| face.weight == Weight::NORMAL && face.stretch == Stretch::Normal);
let has_bold = v.iter().any(|face| face.weight == Weight::BOLD && face.stretch == Stretch::Normal);
has_normal && has_bold
});
font_name_faces_map
};
let font_names = font_name_faces_map.keys().cloned().collect();
let mut font_size_names = Vec::new();
let mut font_sizes = Vec::new();
@ -387,6 +491,42 @@ impl Application for App {
font_sizes.push(font_size);
}
let mut all_font_weights_vals_names_map = BTreeMap::new();
macro_rules! populate_font_weights {
($($weight:ident,)+) => {
// all weights
paste::paste!{
$(
all_font_weights_vals_names_map
.insert(Weight::$weight.0, stringify!([<$weight:camel>]).into());
)+
}
};
}
populate_font_weights!{
THIN, EXTRA_LIGHT, LIGHT, NORMAL, MEDIUM,
SEMIBOLD, BOLD, EXTRA_BOLD, BLACK,
};
let mut all_font_stretches_vals_names_map= BTreeMap::new();
macro_rules! populate_font_stretches {
($($stretch:ident,)+) => {
// all stretches
$(
all_font_stretches_vals_names_map
.insert(Stretch::$stretch, stringify!($stretch).into());
)+
};
}
populate_font_stretches!{
UltraCondensed, ExtraCondensed, Condensed, SemiCondensed,
Normal, SemiExpanded, Expanded, ExtraExpanded, UltraExpanded,
};
let mut zoom_step_names = Vec::new();
let mut zoom_steps = Vec::new();
for zoom_step in [25, 50, 75, 100, 150, 200] {
@ -407,6 +547,13 @@ impl Application for App {
font_names,
font_size_names,
font_sizes,
font_name_faces_map,
all_font_weights_vals_names_map,
all_font_stretches_vals_names_map,
curr_font_weight_names: Vec::new(),
curr_font_weights: Vec::new(),
curr_font_stretch_names: Vec::new(),
curr_font_stretches: Vec::new(),
zoom_adj: 0,
zoom_step_names,
zoom_steps,
@ -417,6 +564,7 @@ impl Application for App {
term_event_tx_opt: None,
};
app.set_curr_font_weights_and_stretches();
let command = app.update_title();
(app, command)
@ -468,6 +616,8 @@ impl Application for App {
}
self.config.font_name = font_name.to_string();
self.set_curr_font_weights_and_stretches();
return self.save_config();
}
}
@ -486,6 +636,34 @@ impl Application for App {
log::warn!("failed to find font with index {}", index);
}
},
Message::DefaultFontStretch(index) => match self.curr_font_stretches.get(index) {
Some(font_stretch) => {
self.config.font_stretch = font_stretch.to_number();
self.set_curr_font_weights_and_stretches();
return self.save_config();
}
None => {
log::warn!("failed to find font weight with index {}", index);
}
},
Message::DefaultFontWeight(index) => match self.curr_font_weights.get(index) {
Some(font_weight) => {
self.config.font_weight = *font_weight;
return self.save_config();
}
None => {
log::warn!("failed to find font weight with index {}", index);
}
},
Message::DefaultBoldFontWeight(index) => match self.curr_font_weights.get(index) {
Some(font_weight) => {
self.config.bold_font_weight = *font_weight;
return self.save_config();
}
None => {
log::warn!("failed to find bold font weight with index {}", index);
}
},
Message::DefaultZoomStep(index) => match self.zoom_steps.get(index) {
Some(zoom_step) => {
self.config.font_size_zoom_step_mul_100 = *zoom_step;
@ -602,6 +780,9 @@ impl Application for App {
entity,
term_event_tx.clone(),
self.term_config.clone(),
self.config.typed_font_stretch(),
self.config.font_weight,
self.config.bold_font_weight,
colors.clone(),
);
terminal.set_config(&self.config, &self.themes, self.zoom_adj);

View file

@ -16,7 +16,7 @@ use alacritty_terminal::{
};
use cosmic::{iced::advanced::graphics::text::font_system, widget::segmented_button};
use cosmic_text::{
Attrs, AttrsList, Buffer, BufferLine, CacheKeyFlags, Family, Metrics, Shaping, Weight, Wrap,
Attrs, AttrsList, Buffer, BufferLine, CacheKeyFlags, Family, Metrics, Shaping, Weight, Stretch, Wrap,
};
use std::{
borrow::Cow,
@ -121,6 +121,7 @@ pub struct Terminal {
size: Size,
pub term: Arc<FairMutex<Term<EventProxy>>>,
colors: Colors,
bold_font_weight: Weight,
notifier: Notifier,
pub context_menu: Option<cosmic::iced::Point>,
pub needs_update: bool,
@ -132,12 +133,17 @@ impl Terminal {
entity: segmented_button::Entity,
event_tx: mpsc::Sender<(segmented_button::Entity, Event)>,
config: Config,
font_stretch: Stretch,
font_weight: u16,
bold_font_weight: u16,
colors: Colors,
) -> Self {
let metrics = Metrics::new(14.0, 20.0);
//TODO: set color to default fg
let default_attrs = Attrs::new()
.family(Family::Monospace)
.weight(Weight(font_weight))
.stretch(font_stretch)
.color(convert_color(&colors, Color::Named(NamedColor::Foreground)))
.metadata(convert_color(&colors, Color::Named(NamedColor::Background)).0 as usize);
let mut buffer = Buffer::new_empty(metrics);
@ -177,6 +183,7 @@ impl Terminal {
Self {
colors,
bold_font_weight: Weight(bold_font_weight),
default_attrs,
buffer: Arc::new(buffer),
size,
@ -327,6 +334,22 @@ impl Terminal {
let mut update_cell_size = false;
let mut update = false;
if self.default_attrs.stretch != config.typed_font_stretch() {
self.default_attrs = self.default_attrs.stretch(config.typed_font_stretch());
update_cell_size = true;
}
if self.default_attrs.weight.0 != config.font_weight {
self.default_attrs = self.default_attrs.weight(Weight(config.font_weight));
update_cell_size = true;
}
if self.bold_font_weight.0 != config.font_weight {
self.bold_font_weight = Weight(config.bold_font_weight);
update_cell_size = true;
}
let metrics = config.metrics(zoom_adj);
if metrics != self.buffer.metrics() {
{
@ -347,6 +370,8 @@ impl Terminal {
if changed {
self.default_attrs = Attrs::new()
.family(Family::Monospace)
.weight(Weight(config.font_weight))
.stretch(config.typed_font_stretch())
.color(convert_color(&colors, Color::Named(NamedColor::Foreground)))
.metadata(
convert_color(&colors, Color::Named(NamedColor::Background)).0 as usize,
@ -475,7 +500,7 @@ impl Terminal {
attrs = attrs.metadata(bg.0 as usize);
//TODO: more flags
if indexed.cell.flags.contains(Flags::BOLD) {
attrs = attrs.weight(Weight::BOLD);
attrs = attrs.weight(self.bold_font_weight);
}
if indexed.cell.flags.contains(Flags::ITALIC) {
//TODO: automatically use fake italic