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 }