mod.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
---
mod.rs (17863B)
---
1 use std::{
2 path::{Path, PathBuf},
3 sync::{mpsc::Receiver, Arc},
4 time::Instant,
5 };
6
7 use crate::{model::Tool, AnsiEditor, ClipboardHandler, Document, DocumentOptions, Message, TerminalResult, UndoHandler};
8 use eframe::{
9 egui::{self, Id, ImageButton, RichText, Slider, TextEdit, TopBottomPanel},
10 epaint::Vec2,
11 };
12 use egui::{Image, ProgressBar};
13 use egui_code_editor::{CodeEditor, Syntax};
14 use i18n_embed_fl::fl;
15 use icy_engine::{ascii, AttributedChar, Buffer, EngineResult, Size, TextAttribute, UnicodeConverter};
16 use icy_engine_egui::{animations::Animator, show_terminal_area, BufferView, MonitorSettings};
17
18 use self::encoding::{start_encoding_thread, ENCODERS};
19 mod asciicast_encoder;
20 mod encoding;
21 mod gif_encoder;
22 mod highlighting;
23 //mod mp4_encoder;
24
25 pub struct AnimationEditor {
26 gl: Arc<glow::Context>,
27 id: usize,
28
29 undostack: usize,
30
31 txt: String,
32 buffer_view: Arc<eframe::epaint::mutex::Mutex<BufferView>>,
33 animator: Arc<std::sync::Mutex<Animator>>,
34 next_animator: Option<Arc<std::sync::Mutex<Animator>>>,
35 set_frame: usize,
36
37 parent_path: Option<PathBuf>,
38 export_path: PathBuf,
39 export_type: usize,
40
41 first_frame: bool,
42
43 shedule_update: bool,
44 last_update: Instant,
45 cursor_index: usize,
46 scale: f32,
47
48 rx: Option<Receiver<usize>>,
49 thread: Option<std::thread::JoinHandle<TerminalResult<()>>>,
50 cur_encoding_frame: usize,
51 encoding_frames: usize,
52 encoding_error: String,
53 }
54
55 impl AnimationEditor {
56 pub fn new(gl: Arc<glow::Context>, id: usize, path: &Path, txt: String) -> Self {
57 let mut buffer = Buffer::new(Size::new(80, 25));
58 buffer.is_terminal_buffer = false;
59 let mut buffer_view = BufferView::from_buffer(&gl, buffer);
60 buffer_view.interactive = false;
61 let buffer_view = Arc::new(eframe::epaint::mutex::Mutex::new(buffer_view));
62 let parent_path = path.parent().map(|p| p.to_path_buf());
63 let animator = Animator::run(&parent_path, txt.clone());
64 let export_path = path.with_extension("gif");
65 Self {
66 gl,
67 id,
68 buffer_view,
69 animator,
70 txt,
71 undostack: 0,
72 export_path,
73 export_type: 0,
74 parent_path,
75 set_frame: 0,
76 scale: 1.0,
77 next_animator: None,
78 shedule_update: false,
79 last_update: Instant::now(),
80 first_frame: true,
81 rx: None,
82 thread: None,
83 cur_encoding_frame: 0,
84 encoding_frames: 0,
85 cursor_index: 0,
86 encoding_error: String::new(),
87 }
88 }
89
90 fn export(&mut self) -> TerminalResult<()> {
91 if let Some((rx, handle)) = start_encoding_thread(self.export_type, self.gl.clone(), self.export_path.clone(), self.animator.clone())? {
92 self.rx = Some(rx);
93 self.thread = Some(handle);
94 self.encoding_frames = self.animator.lock().unwrap().frames.len();
95 }
96 Ok(())
97 }
98 }
99
100 impl ClipboardHandler for AnimationEditor {
101 fn can_copy(&self) -> bool {
102 false
103 }
104
105 fn copy(&mut self) -> EngineResult<()> {
106 Ok(())
107 }
108
109 fn can_paste(&self) -> bool {
110 false
111 }
112
113 fn paste(&mut self) -> EngineResult<()> {
114 Ok(())
115 }
116 }
117
118 impl UndoHandler for AnimationEditor {
119 fn undo_description(&self) -> Option<String> {
120 None
121 }
122
123 fn can_undo(&self) -> bool {
124 false
125 }
126
127 fn undo(&mut self) -> EngineResult<Option<Message>> {
128 Ok(None)
129 }
130
131 fn redo_description(&self) -> Option<String> {
132 None
133 }
134
135 fn can_redo(&self) -> bool {
136 false
137 }
138
139 fn redo(&mut self) -> EngineResult<Option<Message>> {
140 Ok(None)
141 }
142 }
143
144 impl Document for AnimationEditor {
145 fn default_extension(&self) -> &'static str {
146 "icyanim"
147 }
148
149 fn undo_stack_len(&self) -> usize {
150 self.undostack
151 }
152
153 fn can_paste_char(&self) -> bool {
154 true
155 }
156
157 fn paste_char(&mut self, _ui: &mut eframe::egui::Ui, ch: char) {
158 let ch = ascii::CP437Converter::default().convert_to_unicode(AttributedChar::new(ch, TextAttribute::default()));
159 self.txt.insert(self.cursor_index, ch);
160 if let Some((i, _)) = self.txt.char_indices().nth(self.cursor_index + 1) {
161 self.cursor_index = i;
162 }
163 }
164
165 fn show_ui(&mut self, ui: &mut eframe::egui::Ui, _cur_tool: &mut Box<dyn Tool>, _selected_tool: usize, _options: &DocumentOptions) -> Option<Message> {
166 let mut message = None;
167
168 if self.first_frame && self.animator.lock().unwrap().success() {
169 let animator = &mut self.animator.lock().unwrap();
170 let frame_count = animator.frames.len();
171 if frame_count > 0 {
172 animator.set_cur_frame(self.set_frame);
173 animator.display_frame(self.buffer_view.clone());
174 }
175 self.first_frame = false;
176 }
177 if let Some(next) = &self.next_animator {
178 if next.lock().unwrap().success() || !next.lock().unwrap().error.is_empty() {
179 self.animator = next.clone();
180 self.next_animator = None;
181 let animator = &mut self.animator.lock().unwrap();
182 animator.set_cur_frame(self.set_frame);
183 animator.display_frame(self.buffer_view.clone());
184 }
185 }
186
187 egui::SidePanel::right("movie_panel")
188 .default_width(ui.available_width() / 2.0)
189 .min_width(660.0)
190 .show_inside(ui, |ui| {
191 ui.horizontal(|ui| {
192 if !self.animator.lock().unwrap().error.is_empty() {
193 ui.set_enabled(false);
194 }
195
196 if self.animator.lock().unwrap().success() {
197 let animator = &mut self.animator.lock().unwrap();
198 let frame_count = animator.frames.len();
199 if animator.is_playing() {
200 if ui.add(ImageButton::new(crate::PAUSE_SVG.clone())).clicked() {
201 animator.set_is_playing(false);
202 }
203 } else {
204 let image: &Image<'static> = if animator.get_cur_frame() + 1 < frame_count {
205 &crate::PLAY_SVG
206 } else {
207 &crate::REPLAY_SVG
208 };
209 if ui.add(ImageButton::new(image.clone())).clicked() {
210 if animator.get_cur_frame() + 1 >= frame_count {
211 animator.set_cur_frame(0);
212 }
213 animator.start_playback(self.buffer_view.clone());
214 }
215 }
216 if ui
217 .add_enabled(animator.get_cur_frame() + 1 < frame_count, ImageButton::new(crate::SKIP_NEXT_SVG.clone()))
218 .clicked()
219 {
220 animator.set_cur_frame(frame_count - 1);
221 animator.display_frame(self.buffer_view.clone());
222 }
223 let is_loop = animator.get_is_loop();
224 if ui.add(ImageButton::new(crate::REPEAT_SVG.clone()).selected(is_loop)).clicked() {
225 animator.set_is_loop(!is_loop);
226 }
227
228 let mut cf = animator.get_cur_frame() + 1;
229
230 if frame_count > 0
231 && ui
232 .add(Slider::new(&mut cf, 1..=frame_count).text(fl!(crate::LANGUAGE_LOADER, "animation_of_frame_count", total = frame_count)))
233 .changed()
234 {
235 animator.set_cur_frame(cf - 1);
236 animator.display_frame(self.buffer_view.clone());
237 }
238
239 if ui
240 .add_enabled(animator.get_cur_frame() > 0, ImageButton::new(crate::NAVIGATE_PREV.clone()))
241 .clicked()
242 {
243 let cf = animator.get_cur_frame() - 1;
244 animator.set_cur_frame(cf);
245 animator.display_frame(self.buffer_view.clone());
246 }
247
248 if ui
249 .add_enabled(
250 animator.get_cur_frame() + 1 < animator.frames.len(),
251 ImageButton::new(crate::NAVIGATE_NEXT.clone()),
252 )
253 .clicked()
254 {
255 let cf = animator.get_cur_frame() + 1;
256 animator.set_cur_frame(cf);
257 animator.display_frame(self.buffer_view.clone());
258 }
259
260 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
261 if ui.button(if self.scale < 2.0 { "2x" } else { "1x" }).clicked() {
262 if self.scale < 2.0 {
263 self.scale = 2.0;
264 } else {
265 self.scale = 1.0;
266 }
267 }
268 });
269 }
270 });
271
272 if self.animator.lock().unwrap().success() {
273 let cur_frame = self.animator.lock().unwrap().get_cur_frame();
274
275 let monitor_settings = if let Some((_, settings, _)) = self.animator.lock().unwrap().frames.get(cur_frame) {
276 settings.clone()
277 } else {
278 MonitorSettings::default()
279 };
280 let mut scale = Vec2::splat(self.scale);
281 if self.buffer_view.lock().get_buffer().use_aspect_ratio() {
282 scale.y *= 1.35;
283 }
284 let opt = icy_engine_egui::TerminalOptions {
285 stick_to_bottom: false,
286 scale: Some(scale),
287 monitor_settings,
288 id: Some(Id::new(self.id + 20000)),
289 ..Default::default()
290 };
291 ui.allocate_ui(Vec2::new(ui.available_width(), ui.available_height() - 100.0), |ui| {
292 self.buffer_view.lock().get_caret_mut().set_is_visible(false);
293 let (_, _) = show_terminal_area(ui, self.buffer_view.clone(), opt);
294 });
295 ui.add_space(8.0);
296 }
297
298 if let Some(rx) = &self.rx {
299 if let Ok(x) = rx.recv() {
300 self.cur_encoding_frame = x;
301 }
302
303 ui.label(fl!(
304 crate::LANGUAGE_LOADER,
305 "animation_encoding_frame",
306 cur = self.cur_encoding_frame,
307 total = self.encoding_frames
308 ));
309 ui.add(ProgressBar::new(self.cur_encoding_frame as f32 / self.encoding_frames as f32));
310 if self.cur_encoding_frame >= self.encoding_frames {
311 if let Some(thread) = self.thread.take() {
312 if let Ok(Err(err)) = thread.join() {
313 log::error!("Error during encoding: {err}");
314 self.encoding_error = format!("{err}");
315 }
316 }
317 self.rx = None;
318 } else if let Some(thread) = &self.thread {
319 if thread.is_finished() {
320 if let Err(err) = self.thread.take().unwrap().join() {
321 let msg = if let Some(msg) = err.downcast_ref::<&'static str>() {
322 msg.to_string()
323 } else if let Some(msg) = err.downcast_ref::<String>() {
324 msg.clone()
325 } else {
326 format!("?{:?}", err)
327 };
328 log::error!("Error during encoding: {:?}", msg);
329 self.encoding_error = format!("Thread aborted: {:?}", msg);
330 }
331 self.rx = None;
332 }
333 }
334 } else {
335 ui.horizontal(|ui| {
336 ui.label(fl!(crate::LANGUAGE_LOADER, "animation_editor_path_label"));
337 let mut path_edit = self.export_path.to_str().unwrap().to_string();
338 let response = ui.add(
339 // ui.available_size(),
340 TextEdit::singleline(&mut path_edit).desired_width(f32::INFINITY),
341 );
342 if response.changed() {
343 self.export_path = path_edit.into();
344 }
345 });
346 ui.add_space(8.0);
347 ui.horizontal(|ui| {
348 for (i, enc) in ENCODERS.iter().enumerate() {
349 if ui.selectable_label(self.export_type == i, enc.label()).clicked() {
350 self.export_type = i;
351 self.export_path.set_extension(enc.extension());
352 }
353 }
354
355 if ui.button(fl!(crate::LANGUAGE_LOADER, "animation_editor_export_button")).clicked() {
356 if let Err(err) = self.export() {
357 message = Some(Message::ShowError(format!("Could not export: {}", err)));
358 }
359 }
360 });
361
362 if !self.encoding_error.is_empty() {
363 ui.colored_label(ui.style().visuals.error_fg_color, RichText::new(&self.encoding_error));
364 } else {
365 ui.horizontal(|ui| {
366 ui.small(fl!(crate::LANGUAGE_LOADER, "animation_icy_play_note"));
367 ui.hyperlink_to(RichText::new("Icy Play").small(), "https://github.com/mkrueger/icy_play");
368 });
369 }
370 }
371 });
372
373 egui::CentralPanel::default().show_inside(ui, |ui| {
374 TopBottomPanel::bottom("code_error_bottom_panel").exact_height(200.).show_inside(ui, |ui| {
375 if !self.animator.lock().unwrap().error.is_empty() {
376 ui.colored_label(ui.style().visuals.error_fg_color, RichText::new(&self.animator.lock().unwrap().error).small());
377 } else {
378 egui::ScrollArea::vertical().max_width(f32::INFINITY).show(ui, |ui| {
379 self.animator.lock().unwrap().log.iter().for_each(|line| {
380 ui.horizontal(|ui| {
381 ui.label(RichText::new(format!("Frame {}:", line.frame)).strong());
382 ui.label(RichText::new(&line.text));
383 ui.add_space(ui.available_width());
384 });
385 });
386 });
387 }
388 });
389
390 let r = CodeEditor::default()
391 .id_source("code editor")
392 .with_rows(12)
393 .with_fontsize(14.0)
394 .with_theme(if ui.style().visuals.dark_mode {
395 egui_code_editor::ColorTheme::GITHUB_DARK
396 } else {
397 egui_code_editor::ColorTheme::GITHUB_LIGHT
398 })
399 .with_syntax(highlighting::lua())
400 .with_numlines(true)
401 .show(ui, &mut self.txt);
402 if self.shedule_update && self.last_update.elapsed().as_millis() > 1000 {
403 self.shedule_update = false;
404
405 let path = self.parent_path.clone();
406 let txt = self.txt.clone();
407 self.set_frame = self.animator.lock().unwrap().get_cur_frame();
408 self.next_animator = Some(Animator::run(&path, txt));
409 }
410
411 if let Some(range) = r.cursor_range {
412 if let Some((i, _)) = self.txt.char_indices().nth(range.as_sorted_char_range().start) {
413 self.cursor_index = i;
414 } else {
415 self.cursor_index = 0;
416 }
417 }
418 if r.response.changed {
419 self.shedule_update = true;
420 self.last_update = Instant::now();
421 self.undostack += 1;
422 }
423 });
424
425 let buffer_view = self.buffer_view.clone();
426 if self.animator.lock().unwrap().success() {
427 self.animator.lock().unwrap().update_frame(buffer_view);
428 }
429 message
430 }
431
432 fn get_bytes(&mut self, _path: &Path) -> TerminalResult<Vec<u8>> {
433 Ok(self.txt.as_bytes().to_vec())
434 }
435
436 fn get_ansi_editor_mut(&mut self) -> Option<&mut AnsiEditor> {
437 None
438 }
439
440 fn get_ansi_editor(&self) -> Option<&AnsiEditor> {
441 None
442 }
443
444 fn destroy(&self, gl: &glow::Context) -> Option<Message> {
445 self.buffer_view.lock().destroy(gl);
446 None
447 }
448 }