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::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";
29const 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, hub: &Hub, context: &Context) {
240 hub.send(Event::ImportLibrary {
241 library_index: Some(context.settings.selected_library),
242 })
243 .ok();
244 }
245}
246
247#[inline]
248fn draw_segment(
249 pixmap: &mut Pixmap,
250 ts: &mut TouchState,
251 position: Point,
252 time: f64,
253 pen: &Pen,
254 id: Id,
255 fb_rect: &Rectangle,
256 rq: &mut RenderQueue,
257) {
258 let (start_radius, end_radius) = if pen.dynamic {
259 if time > ts.time {
260 let d = vec2!((position.x - ts.pt.x) as f32, (position.y - ts.pt.y) as f32).length();
261 let speed = d / (time - ts.time) as f32;
262 let base_radius = pen.size as f32 / 2.0;
263 let radius = base_radius
264 * (1.0
265 + (pen.amplitude / base_radius) * speed.clamp(pen.min_speed, pen.max_speed)
266 / (pen.max_speed - pen.min_speed));
267 (ts.radius, radius)
268 } else {
269 (ts.radius, ts.radius)
270 }
271 } else {
272 let radius = pen.size as f32 / 2.0;
273 (radius, radius)
274 };
275
276 let rect = Rectangle::from_segment(
277 ts.pt,
278 position,
279 start_radius.ceil() as i32,
280 end_radius.ceil() as i32,
281 );
282
283 pixmap.draw_segment(ts.pt, position, start_radius, end_radius, pen.color);
284
285 if let Some(render_rect) = rect.intersection(fb_rect) {
286 rq.add(RenderData::no_wait(id, render_rect, UpdateMode::FastMono));
287 }
288
289 ts.pt = position;
290 ts.time = time;
291 ts.radius = end_radius;
292}
293
294impl View for Sketch {
295 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, hub, _bus, rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
296 fn handle_event(
297 &mut self,
298 evt: &Event,
299 hub: &Hub,
300 _bus: &mut Bus,
301 rq: &mut RenderQueue,
302 context: &mut Context,
303 ) -> bool {
304 match *evt {
305 Event::Device(DeviceEvent::Finger {
306 status: FingerStatus::Motion,
307 id,
308 position,
309 time,
310 }) => {
311 if let Some(ts) = self.fingers.get_mut(&id) {
312 draw_segment(
313 &mut self.pixmap,
314 ts,
315 position,
316 time,
317 &self.pen,
318 self.id,
319 &self.rect,
320 rq,
321 );
322 }
323 true
324 }
325 Event::Device(DeviceEvent::Finger {
326 status: FingerStatus::Down,
327 id,
328 position,
329 time,
330 }) => {
331 let radius = self.pen.size as f32 / 2.0;
332 self.fingers
333 .insert(id, TouchState::new(position, time, radius));
334 true
335 }
336 Event::Device(DeviceEvent::Finger {
337 status: FingerStatus::Up,
338 id,
339 position,
340 time,
341 }) => {
342 if let Some(ts) = self.fingers.get_mut(&id) {
343 draw_segment(
344 &mut self.pixmap,
345 ts,
346 position,
347 time,
348 &self.pen,
349 self.id,
350 &self.rect,
351 rq,
352 );
353 }
354 self.fingers.remove(&id);
355 true
356 }
357 Event::ToggleNear(ViewId::TitleMenu, rect) => {
358 self.toggle_title_menu(rect, None, rq, context);
359 true
360 }
361 Event::Select(EntryId::SetPenSize(size)) => {
362 self.pen.size = size;
363 true
364 }
365 Event::Select(EntryId::SetPenColor(color)) => {
366 self.pen.color = color;
367 true
368 }
369 Event::Select(EntryId::TogglePenDynamism) => {
370 self.pen.dynamic = !self.pen.dynamic;
371 true
372 }
373 Event::Select(EntryId::Load(ref name)) => {
374 if let Err(e) = self.load(name) {
375 let msg = format!("Couldn't load sketch: {}).", e);
376 let notif = Notification::new(None, msg, false, hub, rq, context);
377 self.children.push(Box::new(notif) as Box<dyn View>);
378 } else {
379 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
380 }
381 true
382 }
383 Event::Select(EntryId::Refresh) => {
384 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Full));
385 true
386 }
387 Event::Select(EntryId::New) => {
388 self.pixmap.clear(WHITE);
389 self.filename = Local::now().format(FILENAME_PATTERN).to_string();
390 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
391 true
392 }
393 Event::Select(EntryId::Save) => {
394 let mut msg = match self.save() {
395 Err(e) => Some(format!("Can't save sketch: {}.", e)),
396 Ok(..) => {
397 if context.settings.sketch.notify_success {
398 Some(format!("Saved {}.", self.filename))
399 } else {
400 None
401 }
402 }
403 };
404 if let Some(msg) = msg.take() {
405 let notif = Notification::new(None, msg, false, hub, rq, context);
406 self.children.push(Box::new(notif) as Box<dyn View>);
407 }
408 true
409 }
410 Event::Select(EntryId::Quit) => {
411 self.quit(hub, context);
412 hub.send(Event::Back).ok();
413 true
414 }
415 _ => false,
416 }
417 }
418
419 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, fb, _fonts), fields(rect = ?rect)))]
420 fn render(&self, fb: &mut dyn Framebuffer, rect: Rectangle, _fonts: &mut Fonts) {
421 fb.draw_framed_pixmap_halftone(&self.pixmap, &rect, rect.min);
422 }
423
424 fn render_rect(&self, rect: &Rectangle) -> Rectangle {
425 rect.intersection(&self.rect).unwrap_or(self.rect)
426 }
427
428 fn might_rotate(&self) -> bool {
429 false
430 }
431
432 fn is_background(&self) -> bool {
433 true
434 }
435
436 fn rect(&self) -> &Rectangle {
437 &self.rect
438 }
439
440 fn rect_mut(&mut self) -> &mut Rectangle {
441 &mut self.rect
442 }
443
444 fn children(&self) -> &Vec<Box<dyn View>> {
445 &self.children
446 }
447
448 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
449 &mut self.children
450 }
451
452 fn id(&self) -> Id {
453 self.id
454 }
455}