diff --git a/Cargo.lock b/Cargo.lock index 520698f..985c7bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -874,6 +874,7 @@ dependencies = [ "lazy_static", "libcosmic", "log", + "paste", "rust-embed", "serde", "tokio", diff --git a/Cargo.toml b/Cargo.toml index d7aafd7..aaf80d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/i18n/en/cosmic_term.ftl b/i18n/en/cosmic_term.ftl index 775af8d..d372f83 100644 --- a/i18n/en/cosmic_term.ftl +++ b/i18n/en/cosmic_term.ftl @@ -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 diff --git a/src/config.rs b/src/config.rs index c0aec54..802485a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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> = 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] + } } diff --git a/src/main.rs b/src/main.rs index 9915673..d8a3d81 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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), DefaultFont(usize), DefaultFontSize(usize), + DefaultFontStretch(usize), + DefaultFontWeight(usize), + DefaultBoldFontWeight(usize), DefaultZoomStep(usize), Paste(Option), PasteValue(Option, String), @@ -186,6 +189,13 @@ pub struct App { font_names: Vec, font_size_names: Vec, font_sizes: Vec, + font_name_faces_map: BTreeMap>, + all_font_weights_vals_names_map: BTreeMap, + all_font_stretches_vals_names_map: BTreeMap, + curr_font_weight_names: Vec, + curr_font_weights: Vec, + curr_font_stretch_names: Vec, + curr_font_stretches: Vec, zoom_adj: i8, zoom_step_names: Vec, zoom_steps: Vec, @@ -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::>() // 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::>(); + + 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::>() // 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::>(); + + 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 { 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); diff --git a/src/terminal.rs b/src/terminal.rs index 74ac22d..885f067 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -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>>, colors: Colors, + bold_font_weight: Weight, notifier: Notifier, pub context_menu: Option, 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