font_selector.rs - icy_draw - icy_draw is the successor to mystic draw. fork / mirror
 (HTM) git clone https://git.drkhsh.at/icy_draw.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
       font_selector.rs (22095B)
       ---
            1 use std::{fs, io::Read, path::Path};
            2 
            3 use eframe::{
            4     egui::{self, Button, Response, Sense, TextEdit, WidgetText},
            5     epaint::{ahash::HashMap, Color32, FontFamily, FontId, Pos2, Rect, Rounding, Stroke, Vec2},
            6 };
            7 use egui::{load::SizedTexture, Image, TextureHandle};
            8 use egui_modal::Modal;
            9 use i18n_embed_fl::fl;
           10 use icy_engine::{AttributedChar, BitFont, Buffer, TextAttribute, ANSI_FONTS, SAUCE_FONT_NAMES};
           11 use walkdir::WalkDir;
           12 
           13 use crate::{create_image, is_font_extensions, AnsiEditor, Message, Settings, TerminalResult};
           14 
           15 #[derive(Default)]
           16 struct BitfontSource {
           17     pub ansi_slot: Option<usize>,
           18     pub sauce: Option<String>,
           19     pub file_slot: Option<usize>,
           20     pub library: bool,
           21 }
           22 
           23 pub struct FontSelector {
           24     fonts: Vec<(BitFont, BitfontSource)>,
           25     selected_font: i32,
           26 
           27     filter: String,
           28     show_builtin: bool,
           29     show_library: bool,
           30     show_file: bool,
           31     has_file: bool,
           32     show_sauce: bool,
           33     should_add: bool,
           34 
           35     image_cache: HashMap<usize, TextureHandle>,
           36     do_select: bool,
           37     edit_selected_font: bool,
           38     only_sauce_fonts: bool,
           39 }
           40 
           41 impl FontSelector {
           42     pub fn new(editor: &AnsiEditor, should_add: bool) -> Self {
           43         let mut fonts = Vec::new();
           44         for f in SAUCE_FONT_NAMES {
           45             fonts.push((
           46                 BitFont::from_sauce_name(f).unwrap(),
           47                 BitfontSource {
           48                     sauce: Some(f.to_string()),
           49                     ..Default::default()
           50                 },
           51             ));
           52         }
           53 
           54         let only_sauce_fonts = matches!(editor.buffer_view.lock().get_buffer().font_mode, icy_engine::FontMode::Sauce);
           55 
           56         if !only_sauce_fonts {
           57             for slot in 0..ANSI_FONTS {
           58                 let ansi_font = BitFont::from_ansi_font_page(slot).unwrap();
           59                 let mut found = false;
           60                 for (existing_font, src) in &mut fonts {
           61                     if existing_font.get_checksum() == ansi_font.get_checksum() {
           62                         src.ansi_slot = Some(slot);
           63                         found = true;
           64                         break;
           65                     }
           66                 }
           67                 if found {
           68                     continue;
           69                 }
           70                 fonts.push((
           71                     ansi_font,
           72                     BitfontSource {
           73                         ansi_slot: Some(slot),
           74                         ..Default::default()
           75                     },
           76                 ));
           77             }
           78 
           79             if let Ok(font_dir) = Settings::get_font_diretory() {
           80                 for lib_font in FontSelector::load_fonts(font_dir.as_path()) {
           81                     let mut found = false;
           82                     for (existing_font, src) in &mut fonts {
           83                         if existing_font.get_checksum() == lib_font.get_checksum() {
           84                             src.library = true;
           85                             found = true;
           86                             break;
           87                         }
           88                     }
           89                     if found {
           90                         continue;
           91                     }
           92 
           93                     fonts.push((
           94                         lib_font,
           95                         BitfontSource {
           96                             library: true,
           97                             ..Default::default()
           98                         },
           99                     ));
          100                 }
          101             }
          102         }
          103 
          104         let mut selected_font = 0;
          105         let cur_font = editor.buffer_view.lock().get_caret().get_font_page();
          106 
          107         for (id, file_font) in editor.buffer_view.lock().get_buffer().font_iter() {
          108             let mut found = false;
          109             for (index, (existing_font, src)) in fonts.iter_mut().enumerate() {
          110                 if existing_font.get_checksum() == file_font.get_checksum() {
          111                     src.file_slot = Some(*id);
          112                     found = true;
          113                     if *id == cur_font {
          114                         selected_font = index as i32;
          115                     }
          116                     break;
          117                 }
          118             }
          119             if !found {
          120                 if *id == cur_font {
          121                     selected_font = fonts.len() as i32;
          122                 }
          123                 fonts.push((
          124                     file_font.clone(),
          125                     BitfontSource {
          126                         file_slot: Some(*id),
          127                         ..Default::default()
          128                     },
          129                 ));
          130             }
          131         }
          132 
          133         Self {
          134             do_select: false,
          135             fonts,
          136             image_cache: HashMap::default(),
          137             selected_font,
          138             filter: String::new(),
          139             show_builtin: true,
          140             show_library: true,
          141             show_file: true,
          142             has_file: true,
          143             show_sauce: true,
          144             edit_selected_font: false,
          145             should_add,
          146             only_sauce_fonts,
          147         }
          148     }
          149 
          150     pub fn font_library() -> Self {
          151         let mut fonts = Vec::new();
          152         for f in SAUCE_FONT_NAMES {
          153             fonts.push((
          154                 BitFont::from_sauce_name(f).unwrap(),
          155                 BitfontSource {
          156                     sauce: Some(f.to_string()),
          157                     ..Default::default()
          158                 },
          159             ));
          160         }
          161 
          162         for slot in 0..ANSI_FONTS {
          163             let ansi_font = BitFont::from_ansi_font_page(slot).unwrap();
          164             let mut found = false;
          165             for (existing_font, src) in &mut fonts {
          166                 if existing_font.get_checksum() == ansi_font.get_checksum() {
          167                     src.ansi_slot = Some(slot);
          168                     found = true;
          169                     break;
          170                 }
          171             }
          172             if found {
          173                 continue;
          174             }
          175             fonts.push((
          176                 ansi_font,
          177                 BitfontSource {
          178                     ansi_slot: Some(slot),
          179                     ..Default::default()
          180                 },
          181             ));
          182         }
          183 
          184         if let Ok(font_dir) = Settings::get_font_diretory() {
          185             for lib_font in FontSelector::load_fonts(font_dir.as_path()) {
          186                 let mut found = false;
          187                 for (existing_font, src) in &mut fonts {
          188                     if existing_font.get_checksum() == lib_font.get_checksum() {
          189                         src.library = true;
          190                         found = true;
          191                         break;
          192                     }
          193                 }
          194                 if found {
          195                     continue;
          196                 }
          197 
          198                 fonts.push((
          199                     lib_font,
          200                     BitfontSource {
          201                         library: true,
          202                         ..Default::default()
          203                     },
          204                 ));
          205             }
          206         }
          207         Self {
          208             do_select: false,
          209             fonts,
          210             image_cache: HashMap::default(),
          211             selected_font: 0,
          212             filter: String::new(),
          213             show_builtin: true,
          214             show_library: true,
          215             show_file: true,
          216             has_file: false,
          217             show_sauce: true,
          218             edit_selected_font: false,
          219             should_add: false,
          220             only_sauce_fonts: false,
          221         }
          222     }
          223 
          224     pub fn load_fonts(tdf_dir: &Path) -> Vec<BitFont> {
          225         let mut fonts = Vec::new();
          226         let walker = WalkDir::new(tdf_dir).into_iter();
          227         for entry in walker.filter_entry(|e| !crate::model::font_imp::FontTool::is_hidden(e)) {
          228             if let Err(e) = entry {
          229                 log::error!("Can't load font library: {e}");
          230                 break;
          231             }
          232             let Ok(entry) = entry else {
          233                 continue;
          234             };
          235             let path = entry.path();
          236 
          237             if path.is_dir() {
          238                 continue;
          239             }
          240             let extension = path.extension();
          241             if extension.is_none() {
          242                 continue;
          243             }
          244             let Some(extension) = extension else {
          245                 continue;
          246             };
          247             let Some(extension) = extension.to_str() else {
          248                 continue;
          249             };
          250             let ext = extension.to_lowercase().to_string();
          251 
          252             if is_font_extensions(&ext) {
          253                 if let Ok(font) = BitFont::load(path) {
          254                     fonts.push(font);
          255                 }
          256             }
          257 
          258             if ext == "zip" {
          259                 match fs::File::open(path) {
          260                     Ok(mut file) => {
          261                         let mut data = Vec::new();
          262                         file.read_to_end(&mut data).unwrap_or_default();
          263                         FontSelector::read_zip_archive(data, &mut fonts);
          264                     }
          265 
          266                     Err(err) => {
          267                         log::error!("Failed to open zip file: {}", err);
          268                     }
          269                 }
          270             }
          271         }
          272         fonts
          273     }
          274 
          275     fn read_zip_archive(data: Vec<u8>, fonts: &mut Vec<BitFont>) {
          276         let file = std::io::Cursor::new(data);
          277         match zip::ZipArchive::new(file) {
          278             Ok(mut archive) => {
          279                 for i in 0..archive.len() {
          280                     match archive.by_index(i) {
          281                         Ok(mut file) => {
          282                             if let Some(path) = file.enclosed_name() {
          283                                 let file_name = path.to_string_lossy().to_string();
          284                                 let ext = path.extension().unwrap().to_str().unwrap();
          285                                 if is_font_extensions(&ext.to_ascii_lowercase()) {
          286                                     let mut data = Vec::new();
          287                                     file.read_to_end(&mut data).unwrap_or_default();
          288                                     if let Ok(font) = BitFont::from_bytes(file_name, &data) {
          289                                         fonts.push(font)
          290                                     }
          291                                 } else if ext == "zip" {
          292                                     let mut data = Vec::new();
          293                                     file.read_to_end(&mut data).unwrap_or_default();
          294                                     FontSelector::read_zip_archive(data, fonts);
          295                                 }
          296                             }
          297                         }
          298                         Err(err) => {
          299                             log::error!("Error reading zip file: {}", err);
          300                         }
          301                     }
          302                 }
          303             }
          304             Err(err) => {
          305                 log::error!("Error reading zip archive: {}", err);
          306             }
          307         }
          308     }
          309 
          310     pub fn selected_font(&self) -> &BitFont {
          311         let font = &self.fonts[self.selected_font as usize];
          312         &font.0
          313     }
          314 
          315     pub fn draw_font_row(&mut self, ui: &mut egui::Ui, cur_font: usize, row_height: f32, is_selected: bool) -> Response {
          316         let font = &self.fonts[cur_font];
          317         let (id, rect) = ui.allocate_space([ui.available_width(), row_height].into());
          318         let response = ui.interact(rect, id, Sense::click());
          319 
          320         if response.hovered() {
          321             ui.painter()
          322                 .rect_filled(rect.expand(1.0), Rounding::same(4.0), ui.style().visuals.widgets.active.bg_fill);
          323         } else if is_selected {
          324             ui.painter()
          325                 .rect_filled(rect.expand(1.0), Rounding::same(4.0), ui.style().visuals.extreme_bg_color);
          326         }
          327 
          328         let text_color = if is_selected {
          329             ui.style().visuals.strong_text_color()
          330         } else {
          331             ui.style().visuals.text_color()
          332         };
          333 
          334         let font_id = FontId::new(14.0, FontFamily::Proportional);
          335         let text: WidgetText = font.0.name.clone().into();
          336         let galley = text.into_galley(ui, Some(false), f32::INFINITY, font_id);
          337         ui.painter().galley_with_override_text_color(
          338             egui::Align2::LEFT_TOP.align_size_within_rect(galley.size(), rect.shrink(4.0)).min,
          339             galley,
          340             text_color,
          341         );
          342 
          343         #[allow(clippy::map_entry)]
          344         if !self.image_cache.contains_key(&cur_font) {
          345             let mut buffer = Buffer::new((64, 4));
          346             buffer.set_font(0, font.0.clone());
          347             for ch in 0..256 {
          348                 buffer.layers[0].set_char(
          349                     (ch % 64, ch / 64),
          350                     AttributedChar::new(unsafe { char::from_u32_unchecked(ch as u32) }, TextAttribute::default()),
          351                 );
          352             }
          353             let img = create_image(ui.ctx(), &buffer);
          354             self.image_cache.insert(cur_font, img);
          355         }
          356 
          357         if let Some(image) = self.image_cache.get(&cur_font) {
          358             let sized_texture: SizedTexture = image.into();
          359             let w = sized_texture.size.x.floor();
          360             let h = sized_texture.size.y.floor();
          361             let r = Rect::from_min_size(Pos2::new((rect.left() + 4.0).floor(), (rect.top() + 24.0).floor()), Vec2::new(w, h));
          362             let image = Image::from_texture(sized_texture);
          363             image.paint_at(ui, r);
          364 
          365             let mut rect = rect;
          366             if font.1.library {
          367                 let left = print_source(fl!(crate::LANGUAGE_LOADER, "font_selector-library_font"), ui, rect, text_color);
          368                 rect.set_right(left);
          369             }
          370 
          371             if font.1.sauce.is_some() {
          372                 let left = print_source(fl!(crate::LANGUAGE_LOADER, "font_selector-sauce_font"), ui, rect, text_color);
          373                 rect.set_right(left);
          374             }
          375 
          376             if font.1.ansi_slot.is_some() {
          377                 let left = print_source(fl!(crate::LANGUAGE_LOADER, "font_selector-ansi_font"), ui, rect, text_color);
          378                 rect.set_right(left);
          379             }
          380 
          381             if font.1.file_slot.is_some() {
          382                 let left = print_source(fl!(crate::LANGUAGE_LOADER, "font_selector-file_font"), ui, rect, text_color);
          383                 rect.set_right(left);
          384             }
          385         }
          386         response
          387     }
          388 }
          389 
          390 fn print_source(font_type: String, ui: &egui::Ui, rect: Rect, text_color: Color32) -> f32 {
          391     let font_id = FontId::new(12.0, FontFamily::Proportional);
          392     let text: WidgetText = font_type.into();
          393     let galley = text.into_galley(ui, Some(false), f32::INFINITY, font_id);
          394     let galley_size = galley.size();
          395 
          396     let left_side = rect.right() - galley_size.x - 10.0;
          397     let rect = Rect::from_min_size(Pos2::new((left_side).floor(), (rect.top() + 8.0).floor()), galley_size);
          398 
          399     ui.painter()
          400         .rect_filled(rect.expand(2.0), Rounding::same(4.0), ui.style().visuals.widgets.active.bg_fill);
          401 
          402     ui.painter().rect_stroke(rect.expand(2.0), 4.0, Stroke::new(1.0, text_color));
          403 
          404     ui.painter()
          405         .galley_with_override_text_color(egui::Align2::CENTER_CENTER.align_size_within_rect(galley_size, rect).min, galley, text_color);
          406     left_side
          407 }
          408 
          409 impl crate::ModalDialog for FontSelector {
          410     fn show(&mut self, ctx: &egui::Context) -> bool {
          411         let mut result = false;
          412         let modal = Modal::new(ctx, "select_font_dialog2");
          413         let font_count = self.fonts.len();
          414         modal.show(|ui| {
          415             modal.title(
          416                 ui,
          417                 if self.should_add {
          418                     fl!(crate::LANGUAGE_LOADER, "add-font-dialog-title", fontcount = font_count)
          419                 } else {
          420                     fl!(crate::LANGUAGE_LOADER, "select-font-dialog-title", fontcount = font_count)
          421                 },
          422             );
          423             modal.frame(ui, |ui| {
          424                 let row_height = 200.0 / 2.0;
          425                 ui.horizontal(|ui: &mut egui::Ui| {
          426                     ui.add_sized(
          427                         [250.0, 20.0],
          428                         TextEdit::singleline(&mut self.filter).hint_text(fl!(crate::LANGUAGE_LOADER, "select-font-dialog-filter-text")),
          429                     );
          430                     let response = ui.button("🗙");
          431                     if response.clicked() {
          432                         self.filter.clear();
          433                     }
          434                     if !self.only_sauce_fonts {
          435                         let response = ui.selectable_label(self.show_library, fl!(crate::LANGUAGE_LOADER, "font_selector-library_font"));
          436                         if response.clicked() {
          437                             self.show_library = !self.show_library;
          438                         }
          439 
          440                         if self.has_file {
          441                             let response = ui.selectable_label(self.show_file, fl!(crate::LANGUAGE_LOADER, "font_selector-file_font"));
          442                             if response.clicked() {
          443                                 self.show_file = !self.show_file;
          444                             }
          445                         }
          446 
          447                         let response = ui.selectable_label(self.show_builtin, fl!(crate::LANGUAGE_LOADER, "font_selector-ansi_font"));
          448                         if response.clicked() {
          449                             self.show_builtin = !self.show_builtin;
          450                         }
          451                         let response = ui.selectable_label(self.show_sauce, fl!(crate::LANGUAGE_LOADER, "font_selector-sauce_font"));
          452                         if response.clicked() {
          453                             self.show_sauce = !self.show_sauce;
          454                         }
          455                     }
          456                 });
          457                 ui.add_space(4.0);
          458 
          459                 let mut filtered_fonts = Vec::new();
          460 
          461                 for i in 0..font_count {
          462                     let font = &self.fonts[i];
          463                     let match_filter = self.show_builtin && font.1.ansi_slot.is_some()
          464                         || self.show_file && font.1.file_slot.is_some()
          465                         || self.show_library && font.1.library
          466                         || self.show_sauce && font.1.sauce.is_some();
          467 
          468                     if font.0.name.to_lowercase().contains(&self.filter.to_lowercase()) && match_filter {
          469                         filtered_fonts.push(i);
          470                     }
          471                 }
          472                 if filtered_fonts.is_empty() {
          473                     if font_count == 0 {
          474                         ui.label(fl!(crate::LANGUAGE_LOADER, "select-font-dialog-no-fonts-installed"));
          475                     } else {
          476                         ui.label(fl!(crate::LANGUAGE_LOADER, "select-font-dialog-no-fonts"));
          477                     }
          478                 } else {
          479                     egui::ScrollArea::vertical()
          480                         .max_height(300.)
          481                         .show_rows(ui, row_height, filtered_fonts.len(), |ui, range| {
          482                             for row in range {
          483                                 let is_selected = self.selected_font == filtered_fonts[row] as i32;
          484                                 let response = self.draw_font_row(ui, filtered_fonts[row], row_height, is_selected);
          485 
          486                                 if response.clicked() {
          487                                     self.selected_font = filtered_fonts[row] as i32;
          488                                 }
          489                                 if response.double_clicked() {
          490                                     self.selected_font = filtered_fonts[row] as i32;
          491                                     self.do_select = true;
          492                                     result = true;
          493                                 }
          494                             }
          495                         });
          496                 }
          497             });
          498 
          499             modal.buttons(ui, |ui| {
          500                 let text = if self.should_add {
          501                     fl!(crate::LANGUAGE_LOADER, "add-font-dialog-select")
          502                 } else {
          503                     fl!(crate::LANGUAGE_LOADER, "select-font-dialog-select")
          504                 };
          505 
          506                 if ui.button(text).clicked() {
          507                     self.do_select = true;
          508                     result = true;
          509                 }
          510                 if ui.button(fl!(crate::LANGUAGE_LOADER, "new-file-cancel")).clicked() {
          511                     result = true;
          512                 }
          513 
          514                 let enabled = self.fonts[self.selected_font as usize].0.path_opt.is_some();
          515                 if ui
          516                     .add_enabled(enabled, Button::new(fl!(crate::LANGUAGE_LOADER, "select-font-dialog-edit-button")))
          517                     .clicked()
          518                 {
          519                     self.edit_selected_font = true;
          520                     result = true;
          521                 }
          522             });
          523         });
          524         modal.open();
          525         result
          526     }
          527 
          528     fn should_commit(&self) -> bool {
          529         self.do_select || self.edit_selected_font
          530     }
          531 
          532     fn commit(&self, editor: &mut AnsiEditor) -> TerminalResult<Option<Message>> {
          533         if self.edit_selected_font {
          534             let font = &self.fonts[self.selected_font as usize];
          535             if let Some(path) = &font.0.path_opt {
          536                 return Ok(Some(Message::TryLoadFile(path.clone())));
          537             }
          538             return Ok(Some(Message::ShowError("Invalid font.".to_string())));
          539         }
          540 
          541         if let Some((font, src)) = self.fonts.get(self.selected_font as usize) {
          542             if let Some(file_slot) = src.file_slot {
          543                 return Ok(Some(Message::SwitchToFontPage(file_slot)));
          544             } else if let Some(ansi_slot) = src.ansi_slot {
          545                 if self.should_add {
          546                     return Ok(Some(Message::AddAnsiFont(ansi_slot)));
          547                 } else {
          548                     return Ok(Some(Message::SetAnsiFont(ansi_slot)));
          549                 }
          550             } else if let Some(name) = &src.sauce {
          551                 if self.should_add {
          552                     return Ok(Some(Message::AddFont(Box::new(font.clone()))));
          553                 } else {
          554                     return Ok(Some(Message::SetSauceFont(name.clone())));
          555                 }
          556             } else {
          557                 let mut font_set = false;
          558                 let mut font_slot = 0;
          559                 editor.buffer_view.lock().get_buffer().font_iter().for_each(|(id, f)| {
          560                     if f == font {
          561                         font_slot = *id;
          562                         font_set = true;
          563                     }
          564                 });
          565                 if font_set {
          566                     return Ok(Some(Message::SwitchToFontPage(font_slot)));
          567                 }
          568 
          569                 if !font_set {
          570                     if self.should_add {
          571                         return Ok(Some(Message::AddFont(Box::new(font.clone()))));
          572                     } else {
          573                         return Ok(Some(Message::SetFont(Box::new(font.clone()))));
          574                     }
          575                 }
          576             }
          577         }
          578 
          579         Ok(None)
          580     }
          581 }