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";
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, 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}