1use crate::engine::{DisplayMode, DisplayOptions, Engine, ViewerOptions};
2use eframe::egui_wgpu;
3use eframe::egui_wgpu::{CallbackResources, ScreenDescriptor};
4use eframe::epaint::PaintCallbackInfo;
5use egui::{Pos2, Rect, Sense, Ui};
6use std::sync::{Arc, Mutex};
7use vsvg::{Document, DocumentTrait, LayerTrait, Length};
8use wgpu::{CommandBuffer, CommandEncoder, Device, Queue, RenderPass};
9
10#[derive(Default)]
19pub struct DocumentWidget {
20 document: Option<Arc<Document>>,
22
23 viewer_options: Arc<Mutex<ViewerOptions>>,
25
26 offset: Pos2,
31
32 scale: f32,
34
35 must_fit_to_view: bool,
37}
38
39static PEN_WIDTHS_MM: &[f32] = &[
40 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.5, 2.0, 3.0,
41 4.0, 5.0,
42];
43
44static PEN_OPACITY_PERCENT: &[u8] = &[100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 5];
45
46impl DocumentWidget {
47 #[must_use]
52 pub(crate) fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Option<Self> {
53 let viewer_options = Arc::new(Mutex::new(ViewerOptions::default()));
54
55 let wgpu_render_state = cc.wgpu_render_state.as_ref()?;
58
59 let engine = Engine::new(wgpu_render_state, viewer_options.clone());
61
62 wgpu_render_state
66 .renderer
67 .write()
68 .callback_resources
69 .insert(engine);
70
71 Some(Self {
72 document: None,
73 viewer_options,
74 offset: Pos2::ZERO,
75 scale: 1.0,
76 must_fit_to_view: true,
77 })
78 }
79
80 pub fn set_document(&mut self, doc: Arc<Document>) {
81 self.document = Some(doc);
82 }
83
84 pub fn set_tolerance(&mut self, tolerance: f64) {
85 self.viewer_options
86 .lock()
87 .unwrap()
88 .display_options
89 .tolerance = tolerance;
90 }
91
92 #[must_use]
93 pub fn antialias(&self) -> f32 {
94 self.viewer_options
95 .lock()
96 .unwrap()
97 .display_options
98 .anti_alias
99 }
100
101 pub fn set_antialias(&self, anti_alias: f32) {
102 self.viewer_options
103 .lock()
104 .unwrap()
105 .display_options
106 .anti_alias = anti_alias;
107 }
108
109 #[must_use]
110 pub fn vertex_count(&self) -> u64 {
111 self.viewer_options.lock().unwrap().vertex_count
112 }
113
114 #[allow(clippy::missing_panics_doc)]
115 pub fn ui(&mut self, ui: &mut Ui) {
116 vsvg::trace_function!();
117
118 let rect = ui.available_rect_before_wrap();
121 let response = ui.interact(rect, ui.id(), Sense::click_and_drag());
122
123 if response.double_clicked() {
125 self.must_fit_to_view = true;
126 }
127
128 if self.must_fit_to_view {
130 self.fit_to_view(&rect);
131 }
132
133 let old_offset = self.offset;
135 let old_scale = self.scale;
136
137 self.offset -= response.drag_delta() / self.scale;
138 if let Some(mut pos) = response.hover_pos() {
139 response.ctx.input(|i| {
140 self.offset -= i.raw_scroll_delta / self.scale;
141 self.scale *= i.zoom_delta();
142 });
143
144 pos -= rect.min.to_vec2();
146 let dz = 1. / old_scale - 1. / self.scale;
147 self.offset += pos.to_vec2() * dz;
148 }
149
150 #[allow(clippy::float_cmp)]
151 if old_offset != self.offset || old_scale != self.scale {
152 self.must_fit_to_view = false;
153 }
154
155 ui.painter().add(egui_wgpu::Callback::new_paint_callback(
157 rect,
158 DocumentWidgetCallback {
159 document: self.document.clone(),
160 origin: cgmath::Point2::new(self.offset.x, self.offset.y),
161 scale: self.scale,
162 rect,
163 },
164 ));
165 }
166
167 #[allow(clippy::too_many_lines)]
168 pub fn view_menu_ui(&mut self, ui: &mut Ui) {
169 ui.menu_button("View", |ui| {
170 ui.set_min_width(200.0);
171
172 ui.menu_button("Display Mode", |ui| {
173 if ui
174 .radio_value(
175 &mut self.viewer_options.lock().unwrap().display_mode,
176 DisplayMode::Preview,
177 "Preview",
178 )
179 .clicked()
180 {
181 ui.close_menu();
182 };
183 if ui
184 .radio_value(
185 &mut self.viewer_options.lock().unwrap().display_mode,
186 DisplayMode::Outline,
187 "Outline",
188 )
189 .clicked()
190 {
191 ui.close_menu();
192 };
193 });
194
195 ui.separator();
196
197 {
198 let pen_width = &mut self
199 .viewer_options
200 .lock()
201 .unwrap()
202 .display_options
203 .line_display_options
204 .override_width;
205 ui.menu_button("Override Pen Width", |ui| {
206 if ui.radio_value(pen_width, None, "Off").clicked() {
207 ui.close_menu();
208 }
209 ui.separator();
210 for width in PEN_WIDTHS_MM {
211 if ui
212 .radio_value(
213 pen_width,
214 Some(Length::mm(*width).into()),
215 format!("{width:.2}mm"),
216 )
217 .clicked()
218 {
219 ui.close_menu();
220 }
221 }
222 });
223 }
224
225 {
226 let opacity = &mut self
227 .viewer_options
228 .lock()
229 .unwrap()
230 .display_options
231 .line_display_options
232 .override_opacity;
233
234 ui.menu_button("Override Pen Opacity", |ui| {
235 if ui.radio_value(opacity, None, "Off").clicked() {
236 ui.close_menu();
237 }
238 ui.separator();
239 for opacity_value in PEN_OPACITY_PERCENT {
240 #[allow(clippy::cast_lossless)]
241 if ui
242 .radio_value(
243 opacity,
244 Some(*opacity_value as f32 / 100.0),
245 format!("{opacity_value}%"),
246 )
247 .clicked()
248 {
249 ui.close_menu();
250 }
251 }
252 });
253 }
254
255 ui.separator();
256
257 ui.checkbox(
258 &mut self
259 .viewer_options
260 .lock()
261 .unwrap()
262 .display_options
263 .show_display_vertices,
264 "Show points",
265 );
266 ui.checkbox(
267 &mut self
268 .viewer_options
269 .lock()
270 .unwrap()
271 .display_options
272 .show_pen_up,
273 "Show pen-up trajectories",
274 );
275 ui.checkbox(
276 &mut self
277 .viewer_options
278 .lock()
279 .unwrap()
280 .display_options
281 .show_bezier_handles,
282 "Show control points",
283 );
284 ui.separator();
285 if ui.button("Fit to view").clicked() {
286 self.must_fit_to_view = true;
287 ui.close_menu();
288 }
289
290 ui.separator();
291
292 ui.horizontal(|ui| {
293 ui.label("AA:");
294 ui.add(egui::Slider::new(
295 &mut self
296 .viewer_options
297 .lock()
298 .unwrap()
299 .display_options
300 .anti_alias,
301 0.0..=2.0,
302 ))
303 .on_hover_text("Renderer anti-aliasing (default: 0.5)");
304 });
305
306 ui.horizontal(|ui| {
307 ui.label("Tol:");
308 ui.add(
309 egui::Slider::new(
310 &mut self
311 .viewer_options
312 .lock()
313 .unwrap()
314 .display_options
315 .tolerance,
316 0.001..=10.0,
317 )
318 .logarithmic(true),
319 )
320 .on_hover_text("Tolerance for rendering curves (default: 0.01)");
321 });
322
323 ui.separator();
324
325 if ui
326 .button("Reset")
327 .on_hover_text("Reset all display options to the default")
328 .clicked()
329 {
330 let options = &mut self.viewer_options.lock().unwrap().display_options;
331 *options = DisplayOptions {
332 anti_alias: options.anti_alias,
333 ..DisplayOptions::default()
334 };
335 ui.close_menu();
336 }
337 });
338 }
339
340 #[allow(clippy::missing_panics_doc)]
341 pub fn layer_menu_ui(&mut self, ui: &mut Ui) {
342 ui.menu_button("Layer", |ui| {
343 let Some(document) = self.document.clone() else {
344 return;
345 };
346
347 for (lid, layer) in &document.layers {
348 let mut viewer_options = self.viewer_options.lock().unwrap();
349 let visibility = viewer_options.layer_visibility.entry(*lid).or_insert(true);
350 let mut label = format!("Layer {lid}");
351 if let Some(name) = &layer.metadata().name {
352 label.push_str(&format!(": {name}"));
353 }
354
355 ui.checkbox(visibility, label);
356 }
357 });
358 }
359
360 fn fit_to_view(&mut self, viewport: &Rect) {
361 vsvg::trace_function!();
362
363 let Some(document) = self.document.clone() else {
364 return;
365 };
366
367 let bounds = if let Some(page_size) = document.metadata().page_size {
368 if page_size.w() != 0.0 && page_size.h() != 0.0 {
369 Some(kurbo::Rect::from_points(
370 (0., 0.),
371 (page_size.w(), page_size.h()),
372 ))
373 } else {
374 document.bounds()
375 }
376 } else {
377 document.bounds()
378 };
379
380 if bounds.is_none() {
381 return;
382 }
383 let bounds = bounds.expect("bounds is not none");
384
385 #[allow(clippy::cast_possible_truncation)]
386 {
387 let (w, h) = (bounds.width() as f32, bounds.height() as f32);
388 let (view_w, view_h) = (viewport.width(), viewport.height());
389
390 self.scale = 0.95 * f32::min(view_w / w, view_h / h);
391
392 self.offset = Pos2::new(
393 bounds.x0 as f32 - (view_w / self.scale - w) / 2.0,
394 bounds.y0 as f32 - (view_h / self.scale - h) / 2.0,
395 );
396 }
397 }
398}
399
400struct DocumentWidgetCallback {
401 document: Option<Arc<Document>>,
402 origin: cgmath::Point2<f32>,
403 scale: f32,
404 rect: Rect,
405}
406
407impl egui_wgpu::CallbackTrait for DocumentWidgetCallback {
408 fn prepare(
409 &self,
410 device: &Device,
411 queue: &Queue,
412 _screen_descriptor: &ScreenDescriptor,
413 _egui_encoder: &mut CommandEncoder,
414 callback_resources: &mut CallbackResources,
415 ) -> Vec<CommandBuffer> {
416 vsvg::trace_scope!("wgpu prepare callback");
417 let engine: &mut Engine = callback_resources.get_mut().unwrap();
418
419 if let Some(document) = self.document.clone() {
420 engine.set_document(document);
421 }
422
423 engine.prepare(device, queue, self.rect, self.scale, self.origin);
424
425 Vec::new()
426 }
427
428 fn paint<'a>(
429 &'a self,
430 _info: PaintCallbackInfo,
431 render_pass: &mut RenderPass<'a>,
432 callback_resources: &'a CallbackResources,
433 ) {
434 vsvg::trace_scope!("wgpu paint callback");
435
436 let engine: &Engine = callback_resources.get().unwrap();
437 engine.paint(render_pass);
438 }
439}