select_tdf_font_dialog.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
---
select_tdf_font_dialog.rs (13216B)
---
1 use std::{fs, sync::Arc};
2
3 use eframe::{
4 egui::{self, Response, Sense, TextEdit, TextStyle, WidgetText},
5 epaint::{ahash::HashMap, ColorImage, FontFamily, FontId, Pos2, Rect, Rounding, Stroke, Vec2},
6 };
7 use egui::{load::SizedTexture, mutex::Mutex, Image, TextureHandle, TextureOptions};
8 use egui_file::FileDialog;
9 use egui_modal::Modal;
10 use i18n_embed_fl::fl;
11 use icy_engine::{editor::EditState, Buffer, Rectangle, Size, TextPane, TheDrawFont};
12
13 use crate::{MainWindow, Message};
14
15 pub struct SelectFontDialog {
16 fonts: Arc<Mutex<Vec<TheDrawFont>>>,
17 selected_font_arc: Arc<Mutex<i32>>,
18 selected_font: i32,
19 pub do_select: bool,
20 filter: String,
21 show_outline: bool,
22 show_color: bool,
23 show_block: bool,
24
25 export_data: Option<Vec<u8>>,
26 export_dialog: Option<FileDialog>,
27
28 image_cache: HashMap<usize, TextureHandle>,
29 }
30
31 impl SelectFontDialog {
32 pub fn new(fonts: Arc<Mutex<Vec<TheDrawFont>>>, selected_font_arc: Arc<Mutex<i32>>) -> Self {
33 let selected_font = *selected_font_arc.lock();
34
35 Self {
36 do_select: false,
37 fonts,
38 selected_font_arc,
39 selected_font,
40 filter: String::new(),
41 show_outline: true,
42 show_color: true,
43 show_block: true,
44 image_cache: HashMap::default(),
45 export_dialog: None,
46 export_data: None,
47 }
48 }
49
50 pub fn draw_font_row(&mut self, ui: &mut egui::Ui, cur_font: usize, row_height: f32, is_selected: bool) -> Response {
51 let font = &self.fonts.lock()[cur_font];
52 let (id, rect) = ui.allocate_space([ui.available_width(), row_height].into());
53 let response = ui.interact(rect, id, Sense::click());
54 if response.hovered() {
55 ui.painter()
56 .rect_filled(rect.expand(1.0), Rounding::same(4.0), ui.style().visuals.widgets.active.bg_fill);
57 } else if is_selected {
58 ui.painter()
59 .rect_filled(rect.expand(1.0), Rounding::same(4.0), ui.style().visuals.extreme_bg_color);
60 }
61
62 let text_color = if is_selected {
63 ui.style().visuals.strong_text_color()
64 } else {
65 ui.style().visuals.text_color()
66 };
67
68 let font_id = TextStyle::Button.resolve(ui.style());
69 let text: WidgetText = font.name.clone().into();
70 let galley = text.into_galley(ui, Some(false), f32::INFINITY, font_id);
71 ui.painter().galley_with_override_text_color(
72 egui::Align2::LEFT_TOP.align_size_within_rect(galley.size(), rect.shrink(4.0)).min,
73 galley,
74 text_color,
75 );
76
77 let mut x = 0.;
78 let mut y = 26.;
79 let mut cnt = 0;
80
81 for ch in '!'..='~' {
82 let color = if font.has_char(ch as u8) {
83 ui.style().visuals.strong_text_color()
84 } else {
85 ui.style().visuals.text_color()
86 };
87 let text: WidgetText = ch.to_string().into();
88 let font_id = FontId::new(14.0, FontFamily::Monospace);
89 let galley = text.into_galley(ui, Some(false), f32::INFINITY, font_id);
90
91 let mut rect = rect.shrink(4.0);
92 rect.set_top(rect.top() + y);
93 rect.set_left(rect.left() + x);
94 x += galley.size().x;
95 cnt += 1;
96 if cnt > 31 {
97 y += galley.size().y;
98 x = 0.;
99 cnt = 0;
100 }
101 ui.painter()
102 .galley_with_override_text_color(egui::Align2::LEFT_TOP.align_size_within_rect(galley.size(), rect).min, galley, color);
103 }
104
105 #[allow(clippy::map_entry)]
106 if !self.image_cache.contains_key(&cur_font) {
107 let buffer = Buffer::new((100, 12));
108 let mut state = EditState::from_buffer(buffer);
109
110 let text = fl!(crate::LANGUAGE_LOADER, "select-font-dialog-preview-text");
111 let lowercase = text.to_ascii_lowercase();
112
113 let b = if font.has_char(text.chars().next().unwrap() as u8) {
114 text.bytes()
115 } else {
116 lowercase.bytes()
117 };
118 for ch in b {
119 let opt_size: Option<Size> = font.render(&mut state, ch);
120 if let Some(size) = opt_size {
121 let mut pos = state.get_caret().get_position();
122 pos.x += size.width + font.spaces;
123 state.get_caret_mut().set_position(pos);
124 }
125 }
126 let img = create_image(ui.ctx(), state.get_buffer());
127 self.image_cache.insert(cur_font, img);
128 }
129
130 if let Some(image) = self.image_cache.get(&cur_font) {
131 let sized_texture: SizedTexture = image.into();
132 let w = (sized_texture.size.x / 2.0).floor();
133 let h = (sized_texture.size.y / 2.0).floor();
134 let r = Rect::from_min_size(
135 Pos2::new((rect.right() - w - 4.0).floor(), (rect.top() + ((rect.height() - h) / 2.0)).floor()),
136 Vec2::new(w, h),
137 );
138 let image = Image::from_texture(sized_texture);
139 image.paint_at(ui, r);
140 /*
141 ui.painter().image(
142 image.texture_id(ui.ctx()),
143 r,
144 Rect::from_min_max(Pos2::new(0.0, 0.0), Pos2::new(1.0, 1.0)),
145 Color32::WHITE,
146 );*/
147
148 let font_type = match font.font_type {
149 icy_engine::FontType::Outline => {
150 fl!(crate::LANGUAGE_LOADER, "select-font-dialog-outline-font")
151 }
152 icy_engine::FontType::Block => {
153 fl!(crate::LANGUAGE_LOADER, "select-font-dialog-block-font")
154 }
155 icy_engine::FontType::Color => {
156 fl!(crate::LANGUAGE_LOADER, "select-font-dialog-color-font")
157 }
158 };
159
160 let font_id = FontId::new(12.0, FontFamily::Proportional);
161 let text: WidgetText = font_type.into();
162 let galley = text.into_galley(ui, Some(false), f32::INFINITY, font_id);
163
164 let rect = Rect::from_min_size(
165 Pos2::new((rect.right() - galley.size().x - 10.0).floor(), (rect.top() + 8.0).floor()),
166 galley.size(),
167 );
168
169 ui.painter()
170 .rect_filled(rect.expand(2.0), Rounding::same(4.0), ui.style().visuals.widgets.active.bg_fill);
171
172 ui.painter().rect_stroke(rect.expand(2.0), 4.0, Stroke::new(1.0, text_color));
173
174 ui.painter()
175 .galley_with_override_text_color(egui::Align2::CENTER_CENTER.align_size_within_rect(galley.size(), rect).min, galley, text_color);
176 }
177
178 response
179 }
180 }
181
182 impl crate::ModalDialog for SelectFontDialog {
183 fn show(&mut self, ctx: &egui::Context) -> bool {
184 if let Some(ed) = &mut self.export_dialog {
185 if ed.show(ctx).selected() {
186 if let Some(res) = ed.path() {
187 let mut res = res.to_path_buf();
188 res.set_extension("tdf");
189 if let Some(data) = self.export_data.take() {
190 if let Err(err) = fs::write(res, data) {
191 log::error!("Failed to write font: {}", err);
192 }
193 } else {
194 log::error!("Export data == None");
195 }
196 }
197 self.export_dialog = None
198 } else {
199 return false;
200 }
201 }
202
203 let mut result = false;
204 let modal = Modal::new(ctx, "select_font_dialog2");
205 let font_count = self.fonts.lock().len();
206 modal.show(|ui| {
207 ui.set_width(700.);
208 modal.title(ui, fl!(crate::LANGUAGE_LOADER, "select-font-dialog-title", fontcount = font_count));
209 modal.frame(ui, |ui| {
210 let row_height = 200.0 / 2.0;
211 ui.horizontal(|ui: &mut egui::Ui| {
212 ui.add_sized(
213 [250.0, 20.0],
214 TextEdit::singleline(&mut self.filter).hint_text(fl!(crate::LANGUAGE_LOADER, "select-font-dialog-filter-text")),
215 );
216 let response = ui.button("🗙");
217 if response.clicked() {
218 self.filter.clear();
219 }
220
221 let response = ui.selectable_label(self.show_color, fl!(crate::LANGUAGE_LOADER, "select-font-dialog-color-font"));
222 if response.clicked() {
223 self.show_color = !self.show_color;
224 }
225
226 let response = ui.selectable_label(self.show_block, fl!(crate::LANGUAGE_LOADER, "select-font-dialog-block-font"));
227 if response.clicked() {
228 self.show_block = !self.show_block;
229 }
230
231 let response = ui.selectable_label(self.show_outline, fl!(crate::LANGUAGE_LOADER, "select-font-dialog-outline-font"));
232 if response.clicked() {
233 self.show_outline = !self.show_outline;
234 }
235 });
236 ui.add_space(4.0);
237
238 let mut filtered_fonts = Vec::new();
239
240 for i in 0..font_count {
241 let font = &self.fonts.lock()[i];
242 if font.name.to_lowercase().contains(&self.filter.to_lowercase())
243 && (self.show_outline && matches!(font.font_type, icy_engine::FontType::Outline)
244 || self.show_block && matches!(font.font_type, icy_engine::FontType::Block)
245 || self.show_color && matches!(font.font_type, icy_engine::FontType::Color))
246 {
247 filtered_fonts.push(i);
248 }
249 }
250 if filtered_fonts.is_empty() {
251 if font_count == 0 {
252 ui.label(fl!(crate::LANGUAGE_LOADER, "select-font-dialog-no-fonts-installed"));
253 } else {
254 ui.label(fl!(crate::LANGUAGE_LOADER, "select-font-dialog-no-fonts"));
255 }
256 } else {
257 egui::ScrollArea::vertical()
258 .max_height(300.)
259 .show_rows(ui, row_height, filtered_fonts.len(), |ui, range| {
260 for row in range {
261 let is_selected = self.selected_font == filtered_fonts[row] as i32;
262 let response = self.draw_font_row(ui, filtered_fonts[row], row_height, is_selected);
263
264 if response.clicked() {
265 self.selected_font = filtered_fonts[row] as i32;
266 }
267 if response.double_clicked() {
268 self.selected_font = filtered_fonts[row] as i32;
269 self.do_select = true;
270 result = true;
271 }
272 }
273 });
274 }
275 });
276
277 modal.buttons(ui, |ui| {
278 if ui.button(fl!(crate::LANGUAGE_LOADER, "select-font-dialog-select")).clicked() {
279 self.do_select = true;
280 result = true;
281 }
282 if ui.button(fl!(crate::LANGUAGE_LOADER, "new-file-cancel")).clicked() {
283 result = true;
284 }
285
286 if ui.button(fl!(crate::LANGUAGE_LOADER, "export-button-title")).clicked() {
287 match self.fonts.lock()[self.selected_font as usize].as_tdf_bytes() {
288 Ok(data) => {
289 let mut initial_path = None;
290 crate::set_default_initial_directory_opt(&mut initial_path);
291 let mut dialog = FileDialog::save_file(initial_path);
292 dialog.open();
293 self.export_data = Some(data);
294 self.export_dialog = Some(dialog);
295 }
296 Err(err) => {
297 log::error!("Failed to export font: {}", err);
298 }
299 }
300 }
301 });
302 });
303 modal.open();
304 result
305 }
306
307 fn should_commit(&self) -> bool {
308 self.do_select
309 }
310
311 fn commit_self(&self, _window: &mut MainWindow<'_>) -> crate::TerminalResult<Option<Message>> {
312 *self.selected_font_arc.lock() = self.selected_font;
313 Ok(None)
314 }
315 }
316
317 pub fn create_image(ctx: &egui::Context, buf: &Buffer) -> TextureHandle {
318 let (size, pixels) = buf.render_to_rgba(Rectangle::from(0, 0, buf.get_width(), buf.get_height()));
319 let color_image: ColorImage = ColorImage::from_rgba_premultiplied([size.width as usize, size.height as usize], &pixels);
320 ctx.load_texture("my_texture", color_image, TextureOptions::NEAREST)
321 }