1use crate::any_data::AnyData;
2use crate::common_vec_op::{CodeParser, CommonVecVisualizer, VecLineGen};
3use crate::cus_component::{toggle, CodeEditor};
4use crate::interfaces::{
5 ICodeEditor, IParser, IVisData, IVisDataGenerator, IVisualizer, ParseError,
6};
7use bincode::{Decode, Encode};
8use eframe::{egui, Storage};
9use log::error;
10use std::collections::{BTreeMap, BTreeSet};
11use std::time::Duration;
12use std::vec;
13
14use super::sample_codes_list::SAMPLE_CODES_LIST;
15use crate::egui::Sense;
16use base64::prelude::*;
17
18const WINDOW_NAMES: [[&str; 2]; 6] = [
19 ["๐", "Samples"],
20 ["", ""],
21 ["โ", "Options"],
22 ["๐ข", "Transform"],
23 ["๐", "Code"],
24 ["โน", "About"],
25];
26
27struct MainAppCache {
28 code: AnyData,
29 lines: Vec<Vec<Box<dyn IVisData>>>,
30
31 params: MainAppParams,
32
33 #[cfg(target_arch = "wasm32")]
34 transfer_data: TransferData,
35}
36
37#[derive(Clone, PartialEq, Decode, Encode)]
38struct MainAppParams {
39 vis_progress_anim: bool,
40 vis_progress_anim_dir: bool,
42 vis_progress: i64,
43 vis_progress_max: i64,
44 lcd_coords: bool,
45 show_inter_dash: bool,
46 colorful_block: bool,
47
48 trans_matrix: [[f64; 3]; 3],
49}
50
51#[derive(Clone, PartialEq, Default, Decode, Encode)]
52struct TransferData {
53 code: String,
54 params: Option<MainAppParams>,
55}
56
57impl Default for MainAppParams {
58 fn default() -> Self {
59 Self {
60 vis_progress_anim: false,
61 vis_progress_anim_dir: true,
62 vis_progress: 0,
63 vis_progress_max: 0,
64 lcd_coords: false,
65 show_inter_dash: true,
66 colorful_block: true,
67 trans_matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], }
69 }
70}
71
72impl Default for MainAppCache {
73 fn default() -> Self {
74 Self {
75 code: AnyData::new("".to_owned()),
76 lines: vec![],
77 params: Default::default(),
78
79 #[cfg(target_arch = "wasm32")]
80 transfer_data: Default::default(),
81 }
82 }
83}
84
85pub struct MainApp {
86 code: AnyData,
87 error: Option<ParseError>,
88
89 params: MainAppParams,
90
91 cache: MainAppCache,
92 samples_cache: BTreeMap<&'static str, MainAppCache>,
93 selected_sample: &'static str,
94 hovered_sample: &'static str,
95
96 #[cfg(target_arch = "wasm32")]
97 is_loaded_from_url: bool,
98
99 side_panel_open: bool,
101 panel_status: BTreeSet<String>,
102}
103
104impl Default for MainApp {
105 fn default() -> Self {
106 let mut app = Self {
107 code: AnyData::new(SAMPLE_CODES_LIST[0].1.to_owned()),
108 params: MainAppParams::default(),
109 cache: MainAppCache {
110 code: AnyData::new("".to_owned()),
111 lines: vec![],
112 params: MainAppParams::default(),
113
114 #[cfg(target_arch = "wasm32")]
115 transfer_data: TransferData::default(),
116 },
117 samples_cache: Default::default(),
118
119 error: None,
120
121 #[cfg(target_arch = "wasm32")]
122 is_loaded_from_url: false,
123 side_panel_open: false,
124 panel_status: Default::default(),
125 selected_sample: "",
126 hovered_sample: "",
127 };
128
129 app.side_panel_open = true;
130 app.panel_status.insert(WINDOW_NAMES[2][1].to_owned());
131
132 app
133 }
134}
135
136impl eframe::App for MainApp {
137 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
138 #[cfg(target_arch = "wasm32")]
139 if self.is_loaded_from_url == false {
140 self.load_from_url_search();
141 self.is_loaded_from_url = true;
142 }
143
144 if !self.panel_status.contains(WINDOW_NAMES[0][1]) {
145 self.selected_sample = "";
146 self.hovered_sample = "";
147 }
148
149 if self.params.vis_progress_anim {
150 ctx.request_repaint_after_secs(0.033);
151
152 self.params.vis_progress += if self.params.vis_progress_anim_dir {
153 1
154 } else {
155 -1
156 };
157
158 if self.params.vis_progress > self.params.vis_progress_max {
159 self.params.vis_progress_anim_dir = false;
160 }
161
162 if self.params.vis_progress < 0 {
163 self.params.vis_progress_anim_dir = true;
164 }
165 }
166
167 let about_resp = egui::TopBottomPanel::top("top").show(ctx, |ui| {
168 self.ui_about(ui);
169 });
170 egui::TopBottomPanel::bottom("bottom").show(ctx, |ui| {
171 self.ui_toast_bar(ui);
172 });
173 egui::SidePanel::left("Panels")
174 .resizable(false)
175 .exact_width(if self.side_panel_open { 100.0 } else { 40.0 })
176 .show(ctx, |ui| {
177 self.ui_panels(ui);
178 });
179
180 egui::SidePanel::left("Samples")
181 .resizable(false)
182 .show_animated(ctx, self.panel_status.contains(WINDOW_NAMES[0][1]), |ui| {
183 self.ui_samples_panel(ui);
184 });
185
186 egui::TopBottomPanel::bottom("SampleCodeEditor")
187 .resizable(false)
188 .exact_height(ctx.available_rect().height() / 2.0)
189 .show_animated(ctx, self.panel_status.contains(WINDOW_NAMES[0][1]), |ui| {
190 self.ui_sample_code_editor(ui);
191 });
192
193 let options_resp = egui::TopBottomPanel::top("Options").show_animated(
194 ctx,
195 self.panel_status.contains(WINDOW_NAMES[2][1]),
196 |ui| {
197 self.ui_options_panel(ui);
198 },
199 );
200
201 let mut transform_open = self.panel_status.contains(WINDOW_NAMES[3][1]);
202 egui::Window::new("Transform")
203 .title_bar(false)
204 .open(&mut transform_open)
205 .fixed_size([140.0, 120.0])
206 .anchor(
207 egui::Align2::RIGHT_TOP,
208 [
209 0.0,
210 about_resp.response.rect.height()
211 + options_resp
212 .map(|t| t.response.rect.height())
213 .unwrap_or_default(),
214 ],
215 )
216 .show(ctx, |ui| {
217 self.ui_transform_panel(ui);
218 });
219 if !transform_open {
220 self.panel_status.remove(WINDOW_NAMES[3][1]);
221 }
222
223 if ctx.available_rect().aspect_ratio() < 1.0 {
224 egui::TopBottomPanel::bottom("CodeEditor")
225 .resizable(false)
226 .exact_height(ctx.available_rect().height() / 2.0)
227 .show_animated(ctx, self.panel_status.contains(WINDOW_NAMES[4][1]), |ui| {
228 self.ui_code_editor(ui);
229 });
230 } else {
231 egui::SidePanel::left("CodeEditor")
232 .resizable(false)
233 .exact_width(ctx.available_rect().width() / 2.0)
234 .show_animated(ctx, self.panel_status.contains(WINDOW_NAMES[4][1]), |ui| {
235 self.ui_code_editor(ui);
236 });
237 }
238
239 egui::CentralPanel::default().show(ctx, |ui| {
240 self.ui_visualizer(ui);
241 });
242 }
243
244 fn save(&mut self, _storage: &mut dyn Storage) {
245 #[cfg(target_arch = "wasm32")]
246 self.save_to_url_search();
247 }
248
249 fn auto_save_interval(&self) -> Duration {
250 Duration::from_millis(30)
251 }
252}
253
254impl MainApp {
255 fn ui_toast_bar(&mut self, ui: &mut egui::Ui) {
256 ui.vertical_centered(|ui| {
257 ui.horizontal(|ui| {
258 let info = self.error.as_ref().map_or_else(
259 || "".to_owned(),
260 |e| format!("({}, {}): Error: {}", e.cursor.row + 1, e.cursor.col, e.msg),
261 );
262 let rt = egui::RichText::new(info)
263 .size(20.0)
264 .color(egui::Color32::RED)
265 .text_style(egui::TextStyle::Monospace);
266 ui.label(rt).highlight();
267 });
268 });
269 }
270
271 fn ui_panels(&mut self, ui: &mut egui::Ui) {
272 egui::ScrollArea::vertical().show(ui, |ui| {
273 ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
274 let side_panel_icon = if self.side_panel_open {
275 "๐ Collapse"
276 } else {
277 "๐"
278 };
279 ui.toggle_value(&mut self.side_panel_open, side_panel_icon);
280 ui.separator();
281 for [icon, name] in WINDOW_NAMES {
282 if icon.is_empty() && name.is_empty() {
283 ui.separator();
284 continue;
285 }
286 let mut is_open = self.panel_status.contains(name);
287 ui.toggle_value(
288 &mut is_open,
289 if self.side_panel_open {
290 format!("{icon} {name}")
291 } else {
292 icon.to_owned()
293 },
294 );
295 if is_open {
296 self.panel_status.insert(name.to_owned());
297 } else {
298 self.panel_status.remove(name);
299 }
300 }
301 });
302 });
303 }
304
305 fn ui_samples_panel(&mut self, ui: &mut egui::Ui) {
306 egui::ScrollArea::vertical().show(ui, |ui| {
307 let mut hover_count = 0;
308 let plot_size = if ui.ctx().screen_rect().width() > 1000.0 {
309 250.0
310 } else {
311 ui.ctx().screen_rect().width() / 4.0
312 };
313 ui.set_width(plot_size);
314 for (name, code) in SAMPLE_CODES_LIST {
315 let selected = self.selected_sample == name;
316 egui::containers::Frame::default()
317 .inner_margin(10.0)
318 .outer_margin(10.0)
319 .rounding(10.0)
320 .show(ui, |ui| {
321 let one_sample = ui.vertical_centered(|ui| {
322 ui.vertical_centered(|ui| {
323 let visualizer = CommonVecVisualizer::new([
324 [1.0, 0.0, 0.0],
325 [0.0, 1.0, 0.0],
326 [0.0, 0.0, 1.0],
327 ]);
328
329 if !self.samples_cache.contains_key(name) {
330 self.samples_cache.insert(name, Default::default());
331
332 let v = self.samples_cache.get_mut(name).unwrap();
333
334 let mut generator = VecLineGen::default();
335 let mut parser = CodeParser::new(
336 AnyData::new(code.to_owned()),
337 &mut generator,
338 );
339 let vlg = parser.parse().unwrap_or_else(|e| {
340 error!("Error: {:?}", e);
341 unreachable!("The sample code can't go wrong.");
342 });
343 let lines = vlg.gen(0..vlg.len() as i64);
344 v.lines = lines;
345 }
346
347 let v = self.samples_cache.get(name).unwrap();
348
349 visualizer.plot(
350 ui,
351 v.lines.clone(),
352 false,
353 true,
354 true,
355 false,
356 |plot| {
357 plot.show_axes([false, false])
358 .id(egui::Id::from(name))
359 .width(plot_size)
360 .height(plot_size)
361 .allow_scroll([false, false])
362 .allow_drag([false, false])
363 .allow_zoom([false, false])
364 .show_x(false)
365 .show_y(false)
366 },
367 );
368 ui.add(egui::Label::new(name).truncate());
369 })
370 });
371
372 let response = one_sample.response;
373
374 let visuals = ui.style().interact_selectable(&response, selected);
375
376 let rect = response.rect;
377 let response = ui.allocate_rect(rect, Sense::click());
378 if response.clicked() {
379 if selected {
380 self.selected_sample = ""
381 } else {
382 self.selected_sample = name;
383 }
384 }
385 if response.hovered() {
386 self.hovered_sample = name;
387 hover_count += 1;
388 }
389
390 if selected
391 || response.hovered()
392 || response.highlighted()
393 || response.has_focus()
394 {
395 let rect = rect.expand(10.0);
396 let mut painter = ui.painter_at(rect);
397 let rect = rect.expand(-2.0);
398 painter.rect(
399 rect,
400 10.0,
401 egui::Color32::TRANSPARENT,
402 egui::Stroke::new(2.0, ui.style().visuals.hyperlink_color),
403 );
404 painter.set_opacity(0.3);
405 painter.rect(rect, 10.0, visuals.text_color(), egui::Stroke::NONE);
406 }
407 });
408 }
409 if hover_count == 0 {
410 self.hovered_sample = "";
411 }
412 ui.shrink_width_to_current();
413 });
414 }
415
416 fn ui_sample_code_editor(&mut self, ui: &mut egui::Ui) {
417 if self.selected_sample.is_empty() && self.hovered_sample.is_empty() {
418 return;
419 }
420
421 let sample_to_be_chosen = if !self.hovered_sample.is_empty() {
422 self.hovered_sample
423 } else {
424 self.selected_sample
425 };
426
427 let mut sample_code = AnyData::new(
428 SAMPLE_CODES_LIST
429 .iter()
430 .find(|x| x.0 == sample_to_be_chosen)
431 .unwrap()
432 .1
433 .to_owned(),
434 );
435
436 ui.heading(format!("Sample: {}", sample_to_be_chosen));
437 ui.separator();
438 ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
439 if ui.button("๐ Replaced With THIS ๐").clicked() {
440 self.code = sample_code.clone::<String>();
441 self.panel_status.remove(WINDOW_NAMES[0][1]);
442 }
443 ui.shrink_height_to_current();
444
445 ui.separator();
446
447 if ui.button("๐ Copy Code").clicked() {
448 ui.output_mut(|o| o.copied_text = sample_code.cast_ref::<String>().clone());
449 }
450 if ui.button("๐ Copy URL").clicked() {
451 let transfer_data = TransferData {
452 code: sample_code.cast_ref::<String>().clone(),
453 params: Some(self.params.clone()),
454 };
455 let t = self.create_transfer_url(&transfer_data);
456 ui.output_mut(|o| o.copied_text = format!("https://w-mai.github.io/vegravis/{t}"));
457 }
458 });
459
460 ui.separator();
461
462 CodeEditor {}.show(ui, &mut sample_code, VecLineGen::default().command_syntax());
463 }
464
465 fn ui_options_panel(&mut self, ui: &mut egui::Ui) {
466 ui.horizontal_wrapped(|ui| {
467 ui.allocate_ui_with_layout(
468 egui::Vec2::new(200.0, 20.0),
469 egui::Layout::left_to_right(egui::Align::Center),
470 |ui| {
471 let mut anim_status = self.params.vis_progress_anim;
472 ui.toggle_value(
473 &mut anim_status,
474 if self.params.vis_progress_anim {
475 "โธ"
476 } else {
477 "โถ"
478 },
479 );
480
481 self.params.vis_progress_anim = anim_status;
482 ui.add(
483 egui::Slider::new(
484 &mut self.params.vis_progress,
485 0..=self.params.vis_progress_max,
486 )
487 .text("Progress")
488 .show_value(true),
489 );
490 },
491 );
492 ui.add(toggle("LCD Coordinates", &mut self.params.lcd_coords));
493 ui.add(toggle(
494 "Show Intermediate Dash",
495 &mut self.params.show_inter_dash,
496 ));
497 ui.add(toggle("Colorful Blocks", &mut self.params.colorful_block));
498 });
499 }
500
501 fn ui_transform_panel(&mut self, ui: &mut egui::Ui) {
502 ui.vertical_centered(|ui| {
503 ui.heading("Transform Matrix");
504 egui_extras::TableBuilder::new(ui)
505 .columns(egui_extras::Column::auto(), 3)
506 .body(|mut body| {
507 for i in 0..3 {
508 body.row(30.0, |mut row| {
509 for j in 0..3 {
510 row.col(|ui| {
511 ui.add(
512 egui::DragValue::new(&mut self.params.trans_matrix[i][j])
513 .speed(0.01),
514 )
515 .on_hover_text(format!("m_{i}{j}"));
516 });
517 }
518 });
519 }
520 });
521 });
522 }
523
524 fn ui_code_editor(&mut self, ui: &mut egui::Ui) {
525 ui.heading("Code Editor");
526
527 ui.separator();
528
529 ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
530 if ui.button("๐ Copy Code").clicked() {
531 ui.output_mut(|o| o.copied_text = self.code.cast_ref::<String>().clone());
532 }
533 ui.shrink_height_to_current();
534
535 if ui.button("๐ Copy URL").clicked() {
536 let transfer_data = TransferData {
537 code: self.code.cast_ref::<String>().clone(),
538 params: Some(self.params.clone()),
539 };
540 let t = self.create_transfer_url(&transfer_data);
541 ui.output_mut(|o| o.copied_text = format!("https://w-mai.github.io/vegravis/{t}"));
542 }
543 });
544
545 ui.separator();
546
547 CodeEditor {}.show(ui, &mut self.code, VecLineGen::default().command_syntax());
548 }
549
550 fn ui_visualizer(&mut self, ui: &mut egui::Ui) {
551 if self.selected_sample.is_empty() && self.hovered_sample.is_empty() {
552 let mut has_error = false;
553 if !self.code.equal::<String, String>(&self.cache.code)
554 || self.params != self.cache.params
555 {
556 let mut generator = VecLineGen::default();
557 let mut parser = CodeParser::new(self.code.clone::<String>(), &mut generator);
558 has_error = match parser.parse() {
560 Ok(vlg) => {
561 let ops_count = vlg.len() as i64;
562 self.params.vis_progress_max = ops_count;
563 if !self.code.equal::<String, String>(&self.cache.code) {
564 self.params.vis_progress = ops_count;
565 }
566
567 let parsed = vlg.gen(0..self.params.vis_progress);
568
569 self.cache.lines = parsed.clone();
570 self.cache.code = self.code.clone::<String>();
571 self.cache.params = self.params.clone();
572 false
573 }
574 Err(e) => {
575 error!("Error: {:?}", e);
576 self.error = Some(e);
577 true
578 }
579 }
580 }
581 if !has_error {
582 self.error = None;
583 }
584 CommonVecVisualizer::new(self.params.trans_matrix).plot(
585 ui,
586 self.cache.lines.clone(),
587 has_error,
588 self.params.show_inter_dash,
589 self.params.colorful_block,
590 self.params.lcd_coords,
591 |x| x,
592 );
593 } else {
594 let visualizer =
595 CommonVecVisualizer::new([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
596 let sample_to_be_chosen = if !self.hovered_sample.is_empty() {
597 self.hovered_sample
598 } else {
599 self.selected_sample
600 };
601 let v = self.samples_cache.get(sample_to_be_chosen).unwrap();
602
603 visualizer.plot(ui, v.lines.clone(), false, true, true, false, |plot| plot);
604 }
605 }
606
607 fn ui_about(&mut self, ui: &mut egui::Ui) {
608 const VERSION: &str = env!("CARGO_PKG_VERSION");
609 use egui::special_emojis::GITHUB;
610 ui.horizontal_wrapped(|ui| {
611 egui::widgets::global_theme_preference_switch(ui);
612 ui.separator();
613 ui.heading("Vector Graphics Visualizer");
614
615 ui.separator();
616 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
617 if ui.ctx().screen_rect().width() > 400.0
618 || self.panel_status.contains(WINDOW_NAMES[5][1])
619 {
620 ui.horizontal_wrapped(|ui| {
621 ui.label(format!("Version: {VERSION}"));
622 ui.hyperlink_to("๐Web Version", "https://w-mai.github.io/vegravis/");
623 ui.hyperlink_to(
624 format!("{GITHUB} vegravis on GitHub"),
625 env!("CARGO_PKG_HOMEPAGE"),
626 );
627 });
628 } else if ui.add(egui::Button::new("โน")).clicked() {
629 self.panel_status.insert(WINDOW_NAMES[5][1].to_owned());
630 }
631 });
632 });
633 }
634}
635
636impl MainApp {
637 fn create_transfer_url(&self, transfer_data: &TransferData) -> String {
638 let config = bincode::config::standard();
639 if let Ok(data) = bincode::encode_to_vec(transfer_data, config) {
640 let mut t = BASE64_URL_SAFE_NO_PAD.encode(data);
641
642 t.insert(0, '?');
643 return t;
644 };
645 Default::default()
646 }
647}
648
649#[cfg(target_arch = "wasm32")]
650impl MainApp {
651 fn load_from_url_search(&mut self) {
652 use eframe::web::web_location;
653 let location = web_location();
654 let query = &location.query;
655 if query.is_empty() {
656 return;
657 }
658
659 if let Ok(data) = BASE64_URL_SAFE_NO_PAD.decode(query) {
660 let config = bincode::config::standard();
661 if let Ok((t, _s)) =
662 bincode::decode_from_slice(&data, config) as Result<(TransferData, _), _>
663 {
664 self.code = AnyData::new(t.code);
665 self.params = t.params.unwrap_or_default();
666
667 return;
668 }
669 }
670
671 error!("Invalid query string");
672 }
673
674 fn save_to_url_search(&mut self) {
675 let history = web_sys::window().unwrap().history().unwrap();
676 let transfer_data = TransferData {
677 code: self.code.cast_ref::<String>().clone(),
678 params: Some(self.params.clone()),
679 };
680
681 if self.cache.transfer_data == transfer_data {
682 return;
683 }
684
685 self.cache.transfer_data = transfer_data;
686
687 let t = self.create_transfer_url(&self.cache.transfer_data);
688 if t.is_empty() {
689 return;
690 }
691
692 use eframe::wasm_bindgen::JsValue;
693 history
694 .push_state_with_url(&JsValue::NULL, "", Some(&t))
695 .unwrap()
696 }
697}