select_palette_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_palette_dialog.rs (16314B)
---
1 use std::{fs, io::Read, path::Path};
2
3 use eframe::{
4 egui::{self, Response, Sense, TextEdit, WidgetText},
5 epaint::{Color32, FontFamily, FontId, Pos2, Rect, Rounding, Stroke, Vec2},
6 };
7 use egui_file::FileDialog;
8 use egui_modal::Modal;
9 use i18n_embed_fl::fl;
10 use icy_engine::{Palette, PaletteFormat, PaletteMode, C64_DEFAULT_PALETTE, DOS_DEFAULT_PALETTE, EGA_PALETTE, VIEWDATA_PALETTE, XTERM_256_PALETTE};
11 use walkdir::WalkDir;
12
13 use crate::{to_message, AnsiEditor, Message, Settings, TerminalResult};
14
15 enum PaletteSource {
16 BuiltIn,
17 Library,
18 File,
19 }
20
21 pub struct SelectPaletteDialog {
22 palettes: Vec<(Palette, PaletteSource)>,
23 selected_palette: i32,
24
25 filter: String,
26 show_builtin: bool,
27 show_library: bool,
28 show_file: bool,
29
30 do_select: bool,
31 edit_selected_font: bool,
32
33 export_dialog: Option<FileDialog>,
34 }
35
36 impl SelectPaletteDialog {
37 pub fn new(editor: &AnsiEditor) -> anyhow::Result<Self> {
38 let mut palettes = Vec::new();
39 let mode = editor.buffer_view.lock().get_buffer().palette_mode;
40
41 let mut dos = Palette::from_slice(&DOS_DEFAULT_PALETTE);
42 dos.title = fl!(crate::LANGUAGE_LOADER, "palette_selector-dos_default_palette");
43 add_palette(&mut palettes, mode, (dos, PaletteSource::BuiltIn));
44
45 let mut dos = Palette::from_slice(&DOS_DEFAULT_PALETTE[0..8]);
46 dos.title = fl!(crate::LANGUAGE_LOADER, "palette_selector-dos_default_low_palette");
47 add_palette(&mut palettes, mode, (dos, PaletteSource::BuiltIn));
48
49 let mut dos = Palette::from_slice(&C64_DEFAULT_PALETTE);
50 dos.title = fl!(crate::LANGUAGE_LOADER, "palette_selector-c64_default_palette");
51 add_palette(&mut palettes, mode, (dos, PaletteSource::BuiltIn));
52
53 let mut dos = Palette::from_slice(&EGA_PALETTE);
54 dos.title = fl!(crate::LANGUAGE_LOADER, "palette_selector-ega_default_palette");
55 add_palette(&mut palettes, mode, (dos, PaletteSource::BuiltIn));
56
57 let mut dos = Palette::from_slice(&XTERM_256_PALETTE.map(|p| {
58 let mut col = p.1.clone();
59 col.name = Some(p.0.to_string());
60 col
61 }));
62 dos.title = fl!(crate::LANGUAGE_LOADER, "palette_selector-xterm_default_palette");
63 add_palette(&mut palettes, mode, (dos, PaletteSource::BuiltIn));
64
65 let mut dos = Palette::from_slice(&VIEWDATA_PALETTE[0..8]);
66 dos.title = fl!(crate::LANGUAGE_LOADER, "palette_selector-viewdata_default_palette");
67 add_palette(&mut palettes, mode, (dos, PaletteSource::BuiltIn));
68
69 let mut pal = editor.buffer_view.lock().get_buffer().palette.clone();
70 let mut selected_palette = 0;
71 if let Some(idx) = palettes.iter().position(|p| p.0.are_colors_equal(&pal)) {
72 selected_palette = idx as i32;
73 } else {
74 if pal.title.is_empty() {
75 pal.title = fl!(crate::LANGUAGE_LOADER, "palette_selector-extracted_from_buffer_default_label");
76 }
77 palettes.insert(0, (pal, PaletteSource::File));
78 }
79 if let Ok(palette_dir) = Settings::get_palettes_diretory() {
80 for palette in SelectPaletteDialog::load_palettes(palette_dir.as_path(), mode)? {
81 add_palette(&mut palettes, mode, palette);
82 }
83 }
84
85 Ok(Self {
86 do_select: false,
87 palettes,
88 selected_palette,
89 filter: String::new(),
90 show_builtin: true,
91 show_library: true,
92 show_file: true,
93 edit_selected_font: false,
94 export_dialog: None,
95 })
96 }
97
98 fn load_palettes(tdf_dir: &Path, mode: PaletteMode) -> anyhow::Result<Vec<(Palette, PaletteSource)>> {
99 let mut palettes = Vec::new();
100 let walker = WalkDir::new(tdf_dir).into_iter();
101 for entry in walker.filter_entry(|e| !crate::model::font_imp::FontTool::is_hidden(e)) {
102 if let Err(e) = entry {
103 log::error!("Can't load palette library: {e}");
104 break;
105 }
106 let Ok(entry) = entry else {
107 continue;
108 };
109 let path = entry.path();
110
111 if path.is_dir() {
112 continue;
113 }
114 let Some(extension) = path.extension() else {
115 continue;
116 };
117 let Some(extension) = extension.to_str() else {
118 continue;
119 };
120
121 if let Ok(palette) = Palette::import_palette(path, &fs::read(path)?) {
122 add_palette(&mut palettes, mode, (palette, PaletteSource::Library));
123 }
124 let ext = extension.to_lowercase();
125 if ext == "zip" {
126 match fs::File::open(path) {
127 Ok(mut file) => {
128 let mut data = Vec::new();
129 file.read_to_end(&mut data).unwrap_or_default();
130 SelectPaletteDialog::read_zip_archive(data, &mut palettes, mode);
131 }
132
133 Err(err) => {
134 log::error!("Failed to open zip file: {}", err);
135 return Err(err.into());
136 }
137 }
138 }
139 }
140 Ok(palettes)
141 }
142
143 fn read_zip_archive(data: Vec<u8>, palettes: &mut Vec<(Palette, PaletteSource)>, mode: PaletteMode) {
144 let file = std::io::Cursor::new(data);
145 match zip::ZipArchive::new(file) {
146 Ok(mut archive) => {
147 for i in 0..archive.len() {
148 match archive.by_index(i) {
149 Ok(mut file) => {
150 if let Some(name) = file.enclosed_name() {
151 let file_name_buf = name.to_path_buf();
152 let file_name = file_name_buf.to_string_lossy().to_ascii_lowercase();
153 let mut data = Vec::new();
154 file.read_to_end(&mut data).unwrap_or_default();
155
156 if file_name.ends_with(".zip") {
157 SelectPaletteDialog::read_zip_archive(data, palettes, mode);
158 } else if let Ok(palette) = Palette::import_palette(&file_name_buf, &data) {
159 add_palette(palettes, mode, (palette, PaletteSource::Library));
160 }
161 }
162 }
163 Err(err) => {
164 log::error!("Error reading zip file: {}", err);
165 }
166 }
167 }
168 }
169 Err(err) => {
170 log::error!("Error reading zip archive: {}", err);
171 }
172 }
173 }
174
175 pub fn draw_palette_row(&mut self, ui: &mut egui::Ui, cur_pal: usize, row_height: f32, is_selected: bool) -> Response {
176 let palette = &self.palettes[cur_pal];
177 let (id, rect) = ui.allocate_space([ui.available_width(), row_height].into());
178 let response = ui.interact(rect, id, Sense::click());
179
180 if response.hovered() {
181 ui.painter()
182 .rect_filled(rect.expand(1.0), Rounding::same(4.0), ui.style().visuals.widgets.active.bg_fill);
183 } else if is_selected {
184 ui.painter()
185 .rect_filled(rect.expand(1.0), Rounding::same(4.0), ui.style().visuals.extreme_bg_color);
186 }
187
188 let text_color = if is_selected {
189 ui.style().visuals.strong_text_color()
190 } else {
191 ui.style().visuals.text_color()
192 };
193
194 let font_id = FontId::new(14.0, FontFamily::Proportional);
195 let text: WidgetText = palette.0.title.clone().into();
196 let galley = text.into_galley(ui, Some(false), f32::INFINITY, font_id);
197 ui.painter().galley_with_override_text_color(
198 egui::Align2::LEFT_TOP.align_size_within_rect(galley.size(), rect.shrink(4.0)).min,
199 galley,
200 text_color,
201 );
202
203 let mut color_rect = rect;
204 color_rect.set_top(rect.top() + 22.0);
205 color_rect.set_height(rect.height() - 32.0);
206
207 let mut num_colors = 8;
208 while (palette.0.len() as f32 / num_colors as f32).ceil() > 6.0 {
209 num_colors += 8;
210 }
211 // paint palette preview
212 let w = color_rect.width() / num_colors as f32;
213 let h = color_rect.height() / (palette.0.len() as f32 / num_colors as f32).ceil().max(1.0);
214
215 for i in 0..palette.0.len() {
216 let (r, g, b) = palette.0.get_rgb(i as u32);
217 let rect = Rect::from_min_size(
218 Pos2::new(color_rect.left() + (i % num_colors) as f32 * w, color_rect.top() + (i / num_colors) as f32 * h),
219 Vec2::new(w, h),
220 );
221 ui.painter().rect_filled(rect, Rounding::ZERO, Color32::from_rgb(r, g, b));
222 }
223
224 // paint palette tag
225 let font_type = match palette.1 {
226 PaletteSource::BuiltIn => {
227 fl!(crate::LANGUAGE_LOADER, "select-palette-dialog-builtin_palette")
228 }
229 PaletteSource::Library => {
230 fl!(crate::LANGUAGE_LOADER, "font_selector-library_font")
231 }
232 PaletteSource::File => {
233 fl!(crate::LANGUAGE_LOADER, "font_selector-file_font")
234 }
235 };
236
237 let font_id = FontId::new(12.0, FontFamily::Proportional);
238 let text: WidgetText = font_type.into();
239 let galley = text.into_galley(ui, Some(false), f32::INFINITY, font_id);
240
241 let rect = Rect::from_min_size(
242 Pos2::new((rect.right() - galley.size().x - 10.0).floor(), (rect.top() + 8.0).floor()),
243 galley.size(),
244 );
245 ui.painter()
246 .rect_filled(rect.expand(2.0), Rounding::same(4.0), ui.style().visuals.widgets.active.bg_fill);
247
248 ui.painter().rect_stroke(rect.expand(2.0), 4.0, Stroke::new(1.0, text_color));
249
250 ui.painter()
251 .galley_with_override_text_color(egui::Align2::CENTER_CENTER.align_size_within_rect(galley.size(), rect).min, galley, text_color);
252
253 if palette.0.description.is_empty() {
254 response
255 } else {
256 response.on_hover_ui(|ui| {
257 ui.small(palette.0.description.clone());
258 })
259 }
260 }
261 }
262
263 fn add_palette(palettes: &mut Vec<(Palette, PaletteSource)>, mode: icy_engine::PaletteMode, mut palette: (Palette, PaletteSource)) {
264 match mode {
265 icy_engine::PaletteMode::RGB => {}
266 icy_engine::PaletteMode::Free16 | icy_engine::PaletteMode::Fixed16 => palette.0.resize(16),
267 icy_engine::PaletteMode::Free8 => palette.0.resize(8),
268 };
269 palettes.push(palette);
270 }
271
272 impl crate::ModalDialog for SelectPaletteDialog {
273 fn show(&mut self, ctx: &egui::Context) -> bool {
274 if let Some(ed) = &mut self.export_dialog {
275 if ed.show(ctx).selected() {
276 if let Some(res) = ed.path() {
277 let s = self.selected_palette as usize;
278 if s < self.palettes.len() {
279 let res = res.with_extension("gpl");
280 let data = self.palettes[s].0.export_palette(&PaletteFormat::Gpl);
281 if let Err(err) = fs::write(res, data) {
282 log::error!("Error exporting palette: {err}");
283 }
284 }
285 }
286 self.export_dialog = None
287 } else {
288 return false;
289 }
290 }
291
292 let mut result = false;
293 let modal = Modal::new(ctx, "select_font_dialog2");
294 let palette_count = self.palettes.len();
295 modal.show(|ui| {
296 modal.title(ui, fl!(crate::LANGUAGE_LOADER, "select-palette-dialog-title", count = palette_count));
297 modal.frame(ui, |ui| {
298 let row_height = 200.0 / 2.0;
299 ui.horizontal(|ui: &mut egui::Ui| {
300 ui.add_sized(
301 [250.0, 20.0],
302 TextEdit::singleline(&mut self.filter).hint_text(fl!(crate::LANGUAGE_LOADER, "select-font-dialog-filter-text")),
303 );
304 let response = ui.button("🗙");
305 if response.clicked() {
306 self.filter.clear();
307 }
308
309 let response = ui.selectable_label(self.show_library, fl!(crate::LANGUAGE_LOADER, "font_selector-library_font"));
310 if response.clicked() {
311 self.show_library = !self.show_library;
312 }
313
314 let response = ui.selectable_label(self.show_builtin, fl!(crate::LANGUAGE_LOADER, "select-palette-dialog-builtin_palette"));
315 if response.clicked() {
316 self.show_builtin = !self.show_builtin;
317 }
318 });
319 ui.add_space(4.0);
320
321 let mut filtered_fonts = Vec::new();
322
323 for i in 0..palette_count {
324 let palette = &self.palettes[i];
325 let match_filter = match palette.1 {
326 PaletteSource::BuiltIn => self.show_builtin,
327 PaletteSource::Library => self.show_library,
328 PaletteSource::File => self.show_file,
329 };
330
331 if palette.0.title.to_lowercase().contains(&self.filter.to_lowercase()) && match_filter {
332 filtered_fonts.push(i);
333 }
334 }
335 if filtered_fonts.is_empty() {
336 if palette_count == 0 {
337 ui.label(fl!(crate::LANGUAGE_LOADER, "select-font-dialog-no-fonts-installed"));
338 } else {
339 ui.label(fl!(crate::LANGUAGE_LOADER, "select-palette-dialog-no-matching-palettes"));
340 }
341 } else {
342 egui::ScrollArea::vertical()
343 .max_height(300.)
344 .show_rows(ui, row_height, filtered_fonts.len(), |ui, range| {
345 for row in range {
346 let is_selected = self.selected_palette == filtered_fonts[row] as i32;
347 let response = self.draw_palette_row(ui, filtered_fonts[row], row_height, is_selected);
348
349 if response.clicked() {
350 self.selected_palette = filtered_fonts[row] as i32;
351 }
352 if response.double_clicked() {
353 self.selected_palette = filtered_fonts[row] as i32;
354 self.do_select = true;
355 result = true;
356 }
357 }
358 });
359 }
360 });
361
362 modal.buttons(ui, |ui| {
363 if ui.button(fl!(crate::LANGUAGE_LOADER, "select-font-dialog-select")).clicked() {
364 self.do_select = true;
365 result = true;
366 }
367 if ui.button(fl!(crate::LANGUAGE_LOADER, "new-file-cancel")).clicked() {
368 result = true;
369 }
370
371 if ui.button(fl!(crate::LANGUAGE_LOADER, "export-button-title")).clicked() {
372 let mut initial_path = None;
373 crate::set_default_initial_directory_opt(&mut initial_path);
374 let mut dialog = FileDialog::save_file(initial_path);
375 dialog.open();
376 self.export_dialog = Some(dialog);
377 }
378 });
379 });
380 modal.open();
381 result
382 }
383
384 fn should_commit(&self) -> bool {
385 self.do_select || self.edit_selected_font
386 }
387
388 fn commit(&self, editor: &mut AnsiEditor) -> TerminalResult<Option<Message>> {
389 if let Some((palette, _)) = self.palettes.get(self.selected_palette as usize) {
390 Ok(to_message(editor.buffer_view.lock().get_edit_state_mut().switch_to_palette(palette.clone())))
391 } else {
392 Ok(None)
393 }
394 }
395 }