cadmus_core/view/sketch/
mod.rs

1use crate::color::{Color, BLACK, WHITE};
2use crate::context::Context;
3use crate::device::CURRENT_DEVICE;
4use crate::font::Fonts;
5use crate::framebuffer::{Framebuffer, Pixmap, UpdateMode};
6use crate::geom::{CornerSpec, Point, Rectangle};
7use crate::helpers::IsHidden;
8use crate::input::{DeviceEvent, FingerStatus};
9use crate::settings::{ImportSettings, Pen};
10use crate::unit::scale_by_dpi;
11use crate::view::common::locate_by_id;
12use crate::view::icon::{Icon, ICONS_PIXMAPS};
13use crate::view::menu::{Menu, MenuKind};
14use crate::view::notification::Notification;
15use crate::view::{Bus, Event, Hub, RenderData, RenderQueue, View};
16use crate::view::{EntryId, EntryKind, Id, ViewId, ID_FEEDER};
17use crate::view::{BORDER_RADIUS_SMALL, SMALL_BAR_HEIGHT};
18use anyhow::Error;
19use chrono::Local;
20use fxhash::FxHashMap;
21use globset::Glob;
22use std::fs::{self, File};
23use std::io::BufReader;
24use std::path::PathBuf;
25use walkdir::WalkDir;
26
27const FILENAME_PATTERN: &str = "sketch-%Y%m%d_%H%M%S.png";
28const ICON_NAME: &str = "enclosed_menu";
29// https://oeis.org/A000041
30const PEN_SIZES: [i32; 12] = [1, 2, 3, 5, 7, 11, 15, 22, 30, 42, 56, 77];
31
32struct TouchState {
33    pt: Point,
34    time: f64,
35    radius: f32,
36}
37
38impl TouchState {
39    fn new(pt: Point, time: f64, radius: f32) -> TouchState {
40        TouchState { pt, time, radius }
41    }
42}
43
44pub struct Sketch {
45    id: Id,
46    rect: Rectangle,
47    children: Vec<Box<dyn View>>,
48    pixmap: Pixmap,
49    fingers: FxHashMap<i32, TouchState>,
50    pen: Pen,
51    save_path: PathBuf,
52    filename: String,
53}
54
55impl Sketch {
56    pub fn new(rect: Rectangle, rq: &mut RenderQueue, context: &mut Context) -> Sketch {
57        let id = ID_FEEDER.next();
58        let mut children = Vec::new();
59        let dpi = CURRENT_DEVICE.dpi;
60        let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32;
61        let border_radius = scale_by_dpi(BORDER_RADIUS_SMALL, dpi) as i32;
62        let pixmap = &ICONS_PIXMAPS[ICON_NAME];
63        let icon_padding = (small_height - pixmap.width.max(pixmap.height) as i32) / 2;
64        let width = pixmap.width as i32 + icon_padding;
65        let height = pixmap.height as i32 + icon_padding;
66        let dx = (small_height - width) / 2;
67        let dy = (small_height - height) / 2;
68        let icon_rect = rect![
69            rect.min.x + dx,
70            rect.max.y - dy - height,
71            rect.min.x + dx + width,
72            rect.max.y - dy
73        ];
74        let icon = Icon::new(
75            ICON_NAME,
76            icon_rect,
77            Event::ToggleNear(ViewId::TitleMenu, icon_rect),
78        )
79        .corners(Some(CornerSpec::Uniform(border_radius)));
80        children.push(Box::new(icon) as Box<dyn View>);
81        let save_path = context
82            .library
83            .home
84            .join(&context.settings.sketch.save_path);
85        rq.add(RenderData::new(id, rect, UpdateMode::Full));
86        Sketch {
87            id,
88            rect,
89            children,
90            pixmap: Pixmap::new(rect.width(), rect.height(), 1),
91            fingers: FxHashMap::default(),
92            pen: context.settings.sketch.pen.clone(),
93            save_path,
94            filename: Local::now().format(FILENAME_PATTERN).to_string(),
95        }
96    }
97
98    fn toggle_title_menu(
99        &mut self,
100        rect: Rectangle,
101        enable: Option<bool>,
102        rq: &mut RenderQueue,
103        context: &mut Context,
104    ) {
105        if let Some(index) = locate_by_id(self, ViewId::SketchMenu) {
106            if let Some(true) = enable {
107                return;
108            }
109
110            rq.add(RenderData::expose(
111                *self.child(index).rect(),
112                UpdateMode::Gui,
113            ));
114            self.children.remove(index);
115        } else {
116            if let Some(false) = enable {
117                return;
118            }
119
120            let glob = Glob::new("**/*.png").unwrap().compile_matcher();
121            let mut loadables: Vec<PathBuf> = WalkDir::new(&self.save_path)
122                .min_depth(1)
123                .into_iter()
124                .filter_map(|e| {
125                    e.ok()
126                        .filter(|e| !e.is_hidden())
127                        .and_then(|e| e.path().file_name().map(PathBuf::from))
128                })
129                .filter(|p| glob.is_match(p))
130                .collect();
131            loadables.sort_by(|a, b| b.cmp(a));
132
133            let mut sizes = vec![
134                EntryKind::CheckBox(
135                    "Dynamic".to_string(),
136                    EntryId::TogglePenDynamism,
137                    self.pen.dynamic,
138                ),
139                EntryKind::Separator,
140            ];
141
142            for s in PEN_SIZES.iter() {
143                sizes.push(EntryKind::RadioButton(
144                    s.to_string(),
145                    EntryId::SetPenSize(*s),
146                    self.pen.size == *s,
147                ));
148            }
149
150            let mut colors = vec![
151                EntryKind::RadioButton(
152                    "White".to_string(),
153                    EntryId::SetPenColor(WHITE),
154                    self.pen.color == WHITE,
155                ),
156                EntryKind::RadioButton(
157                    "Black".to_string(),
158                    EntryId::SetPenColor(BLACK),
159                    self.pen.color == BLACK,
160                ),
161            ];
162
163            for i in 1..=14 {
164                let level = i * 17;
165                if i % 7 == 1 {
166                    colors.push(EntryKind::Separator);
167                }
168                let color = Color::Gray(level);
169                colors.push(EntryKind::RadioButton(
170                    format!("Gray {:02}", i),
171                    EntryId::SetPenColor(color),
172                    self.pen.color == color,
173                ));
174            }
175
176            let mut entries = vec![
177                EntryKind::SubMenu("Size".to_string(), sizes),
178                EntryKind::SubMenu("Color".to_string(), colors),
179                EntryKind::Separator,
180                EntryKind::Command("Save".to_string(), EntryId::Save),
181                EntryKind::Command("Refresh".to_string(), EntryId::Refresh),
182                EntryKind::Command("New".to_string(), EntryId::New),
183                EntryKind::Command("Quit".to_string(), EntryId::Quit),
184            ];
185
186            if !loadables.is_empty() {
187                entries.insert(
188                    entries.len() - 1,
189                    EntryKind::SubMenu(
190                        "Load".to_string(),
191                        loadables
192                            .into_iter()
193                            .map(|e| {
194                                EntryKind::Command(
195                                    e.to_string_lossy().into_owned(),
196                                    EntryId::Load(e),
197                                )
198                            })
199                            .collect(),
200                    ),
201                );
202            }
203
204            let sketch_menu = Menu::new(
205                rect,
206                ViewId::SketchMenu,
207                MenuKind::Contextual,
208                entries,
209                context,
210            );
211            rq.add(RenderData::new(
212                sketch_menu.id(),
213                *sketch_menu.rect(),
214                UpdateMode::Gui,
215            ));
216            self.children.push(Box::new(sketch_menu) as Box<dyn View>);
217        }
218    }
219
220    fn load(&mut self, filename: &PathBuf) -> Result<(), Error> {
221        let path = self.save_path.join(filename);
222        let file = File::open(path)?;
223        let decoder = png::Decoder::new(BufReader::new(file));
224        let mut reader = decoder.read_info()?;
225        reader.next_frame(self.pixmap.data_mut())?;
226        self.filename = filename.to_string_lossy().into_owned();
227        Ok(())
228    }
229
230    fn save(&self) -> Result<(), Error> {
231        if !self.save_path.exists() {
232            fs::create_dir_all(&self.save_path)?;
233        }
234        let path = self.save_path.join(&self.filename);
235        self.pixmap.save(&path.to_string_lossy().into_owned())?;
236        Ok(())
237    }
238
239    fn quit(&self, context: &mut Context) {
240        let import_settings = ImportSettings {
241            allowed_kinds: ["png".to_string()].iter().cloned().collect(),
242            ..Default::default()
243        };
244        context.library.import(&import_settings);
245    }
246}
247
248#[inline]
249fn draw_segment(
250    pixmap: &mut Pixmap,
251    ts: &mut TouchState,
252    position: Point,
253    time: f64,
254    pen: &Pen,
255    id: Id,
256    fb_rect: &Rectangle,
257    rq: &mut RenderQueue,
258) {
259    let (start_radius, end_radius) = if pen.dynamic {
260        if time > ts.time {
261            let d = vec2!((position.x - ts.pt.x) as f32, (position.y - ts.pt.y) as f32).length();
262            let speed = d / (time - ts.time) as f32;
263            let base_radius = pen.size as f32 / 2.0;
264            let radius = base_radius
265                * (1.0
266                    + (pen.amplitude / base_radius) * speed.clamp(pen.min_speed, pen.max_speed)
267                        / (pen.max_speed - pen.min_speed));
268            (ts.radius, radius)
269        } else {
270            (ts.radius, ts.radius)
271        }
272    } else {
273        let radius = pen.size as f32 / 2.0;
274        (radius, radius)
275    };
276
277    let rect = Rectangle::from_segment(
278        ts.pt,
279        position,
280        start_radius.ceil() as i32,
281        end_radius.ceil() as i32,
282    );
283
284    pixmap.draw_segment(ts.pt, position, start_radius, end_radius, pen.color);
285
286    if let Some(render_rect) = rect.intersection(fb_rect) {
287        rq.add(RenderData::no_wait(id, render_rect, UpdateMode::FastMono));
288    }
289
290    ts.pt = position;
291    ts.time = time;
292    ts.radius = end_radius;
293}
294
295impl View for Sketch {
296    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, hub, _bus, rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
297    fn handle_event(
298        &mut self,
299        evt: &Event,
300        hub: &Hub,
301        _bus: &mut Bus,
302        rq: &mut RenderQueue,
303        context: &mut Context,
304    ) -> bool {
305        match *evt {
306            Event::Device(DeviceEvent::Finger {
307                status: FingerStatus::Motion,
308                id,
309                position,
310                time,
311            }) => {
312                if let Some(ts) = self.fingers.get_mut(&id) {
313                    draw_segment(
314                        &mut self.pixmap,
315                        ts,
316                        position,
317                        time,
318                        &self.pen,
319                        self.id,
320                        &self.rect,
321                        rq,
322                    );
323                }
324                true
325            }
326            Event::Device(DeviceEvent::Finger {
327                status: FingerStatus::Down,
328                id,
329                position,
330                time,
331            }) => {
332                let radius = self.pen.size as f32 / 2.0;
333                self.fingers
334                    .insert(id, TouchState::new(position, time, radius));
335                true
336            }
337            Event::Device(DeviceEvent::Finger {
338                status: FingerStatus::Up,
339                id,
340                position,
341                time,
342            }) => {
343                if let Some(ts) = self.fingers.get_mut(&id) {
344                    draw_segment(
345                        &mut self.pixmap,
346                        ts,
347                        position,
348                        time,
349                        &self.pen,
350                        self.id,
351                        &self.rect,
352                        rq,
353                    );
354                }
355                self.fingers.remove(&id);
356                true
357            }
358            Event::ToggleNear(ViewId::TitleMenu, rect) => {
359                self.toggle_title_menu(rect, None, rq, context);
360                true
361            }
362            Event::Select(EntryId::SetPenSize(size)) => {
363                self.pen.size = size;
364                true
365            }
366            Event::Select(EntryId::SetPenColor(color)) => {
367                self.pen.color = color;
368                true
369            }
370            Event::Select(EntryId::TogglePenDynamism) => {
371                self.pen.dynamic = !self.pen.dynamic;
372                true
373            }
374            Event::Select(EntryId::Load(ref name)) => {
375                if let Err(e) = self.load(name) {
376                    let msg = format!("Couldn't load sketch: {}).", e);
377                    let notif = Notification::new(None, msg, false, hub, rq, context);
378                    self.children.push(Box::new(notif) as Box<dyn View>);
379                } else {
380                    rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
381                }
382                true
383            }
384            Event::Select(EntryId::Refresh) => {
385                rq.add(RenderData::new(self.id, self.rect, UpdateMode::Full));
386                true
387            }
388            Event::Select(EntryId::New) => {
389                self.pixmap.clear(WHITE);
390                self.filename = Local::now().format(FILENAME_PATTERN).to_string();
391                rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
392                true
393            }
394            Event::Select(EntryId::Save) => {
395                let mut msg = match self.save() {
396                    Err(e) => Some(format!("Can't save sketch: {}.", e)),
397                    Ok(..) => {
398                        if context.settings.sketch.notify_success {
399                            Some(format!("Saved {}.", self.filename))
400                        } else {
401                            None
402                        }
403                    }
404                };
405                if let Some(msg) = msg.take() {
406                    let notif = Notification::new(None, msg, false, hub, rq, context);
407                    self.children.push(Box::new(notif) as Box<dyn View>);
408                }
409                true
410            }
411            Event::Select(EntryId::Quit) => {
412                self.quit(context);
413                hub.send(Event::Back).ok();
414                true
415            }
416            _ => false,
417        }
418    }
419
420    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, _fonts), fields(rect = ?rect)))]
421    fn render(&self, fb: &mut dyn Framebuffer, rect: Rectangle, _fonts: &mut Fonts) {
422        fb.draw_framed_pixmap_halftone(&self.pixmap, &rect, rect.min);
423    }
424
425    fn render_rect(&self, rect: &Rectangle) -> Rectangle {
426        rect.intersection(&self.rect).unwrap_or(self.rect)
427    }
428
429    fn might_rotate(&self) -> bool {
430        false
431    }
432
433    fn is_background(&self) -> bool {
434        true
435    }
436
437    fn rect(&self) -> &Rectangle {
438        &self.rect
439    }
440
441    fn rect_mut(&mut self) -> &mut Rectangle {
442        &mut self.rect
443    }
444
445    fn children(&self) -> &Vec<Box<dyn View>> {
446        &self.children
447    }
448
449    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
450        &mut self.children
451    }
452
453    fn id(&self) -> Id {
454        self.id
455    }
456}