| 1 | //! The Draw tab's tools on the page: pens and highlighters drawing a stroke per press, shapes |
| 2 | //! dragged out on the grid, the stroke eraser, and the lasso whose drawings move and delete, |
| 3 | //! as OneNote 2010 has them (`corpus/ink-tools`). |
| 4 | |
| 5 | use super::{PageView, Response, Result}; |
| 6 | use crate::editor::{FAVORITES, Pen}; |
| 7 | use crate::gpu::colorref; |
| 8 | use draw::{ |
| 9 | Primitive, Stroke, |
| 10 | edit::{Key, NamedKey}, |
| 11 | }; |
| 12 | use onestore::{ |
| 13 | ExGuid, |
| 14 | page::{Ink, InkStroke, ink::ShapeKind, text::new_id}, |
| 15 | }; |
| 16 | |
| 17 | /// The pen gallery under a section of tab colour `section` (linear RGBA): a pen in the |
| 18 | /// section's accent, then OneNote's favourites. The accent takes the light theme's shade in |
| 19 | /// either theme, so a stroke stores one colour and shows it in both. |
| 20 | pub fn pens(section: [f32; 4]) -> [Pen; 15] { |
| 21 | let [red, green, blue] = draw::srgb_bytes({ |
| 22 | let [saturation, lightness] = draw::LIGHT_ACCENT; |
| 23 | draw::hsl( |
| 24 | draw::hue(section), |
| 25 | saturation * draw::vividness(section), |
| 26 | lightness, |
| 27 | ) |
| 28 | }); |
| 29 | let accent = Pen::new(35.0, Some(u32::from_le_bytes([red, green, blue, 0]))); |
| 30 | std::array::from_fn(|place| place.checked_sub(1).map_or(accent, |at| FAVORITES[at])) |
| 31 | } |
| 32 | |
| 33 | /// What a press on the page does. |
| 34 | #[derive(Clone, Copy, Debug, Default, PartialEq)] |
| 35 | pub enum Tool { |
| 36 | /// Select & Type: text, and drawings picked by a click. |
| 37 | #[default] |
| 38 | Select, |
| 39 | /// A stroke per press. |
| 40 | Pen(Pen), |
| 41 | /// A shape dragged out, its corners on the grid unless Option is held. |
| 42 | Shape(ShapeKind, Pen), |
| 43 | /// The stroke eraser. |
| 44 | Eraser, |
| 45 | Lasso, |
| 46 | } |
| 47 | |
| 48 | #[derive(Default)] |
| 49 | pub(super) struct State { |
| 50 | tool: Tool, |
| 51 | gesture: Option<Gesture>, |
| 52 | /// Drawings picked by the lasso or a click. |
| 53 | selection: Vec<ExGuid>, |
| 54 | /// The pen's pressure where the pointer is, from 0 to 1; none for a mouse or finger. |
| 55 | pressure: Option<f32>, |
| 56 | } |
| 57 | |
| 58 | enum Gesture { |
| 59 | /// The points drawn through, and the pressure at each where the pen reports it. |
| 60 | Stroke(Vec<[f32; 2]>, Vec<f32>), |
| 61 | Shape([f32; 2]), |
| 62 | /// `mark` counts the ops waiting when the sweep began. |
| 63 | Erase { |
| 64 | last: [f32; 2], |
| 65 | erased: bool, |
| 66 | mark: usize, |
| 67 | }, |
| 68 | Lasso(Vec<[f32; 2]>), |
| 69 | Move { |
| 70 | press: [f32; 2], |
| 71 | }, |
| 72 | } |
| 73 | |
| 74 | /// The eraser's reach either side of its path, in logical pixels. |
| 75 | const ERASER_REACH: f32 = 4.0; |
| 76 | |
| 77 | impl PageView { |
| 78 | pub fn tool(&self) -> Tool { |
| 79 | self.ink.tool |
| 80 | } |
| 81 | |
| 82 | /// Picks what presses on the page do, as the Draw tab's tools do; the lasso's selection |
| 83 | /// goes. |
| 84 | pub fn set_tool(&mut self, tool: Tool) -> Response { |
| 85 | self.ink.tool = tool; |
| 86 | self.leave_ink(); |
| 87 | Response::redraw() |
| 88 | } |
| 89 | |
| 90 | /// The pen's pressure at the pointer, from 0 to 1, for what the pointer does next: none |
| 91 | /// for a mouse or finger, whose strokes keep their pen's width. |
| 92 | pub fn set_pressure(&mut self, pressure: Option<f32>) { |
| 93 | self.ink.pressure = pressure; |
| 94 | } |
| 95 | |
| 96 | /// Drawings the lasso or a click picked. |
| 97 | pub fn ink_selection(&self) -> &[ExGuid] { |
| 98 | &self.ink.selection |
| 99 | } |
| 100 | |
| 101 | /// Ends a gesture as though it never began, as a second finger landing does on a touch |
| 102 | /// screen: a sweep of the eraser gives back what it erased. The sweep's ops must not have |
| 103 | /// been taken since it began. |
| 104 | pub fn cancel_ink(&mut self) -> Result<Response> { |
| 105 | let gesture = self.ink.gesture.take(); |
| 106 | self.editor.drag_ink(None); |
| 107 | if let Some(Gesture::Erase { |
| 108 | erased: true, mark, .. |
| 109 | }) = gesture |
| 110 | { |
| 111 | self.editor.retract_erasing(&mut self.engine, mark)?; |
| 112 | } |
| 113 | self.changed() |
| 114 | } |
| 115 | |
| 116 | /// Ends a gesture unfinished, as a lost focus does. |
| 117 | pub(super) fn end_ink(&mut self) { |
| 118 | self.ink.gesture = None; |
| 119 | self.editor.drag_ink(None); |
| 120 | } |
| 121 | |
| 122 | /// Another page shows: the gesture ends and nothing stays picked; the tool stays. |
| 123 | pub(super) fn leave_ink(&mut self) { |
| 124 | self.end_ink(); |
| 125 | self.ink.selection.clear(); |
| 126 | } |
| 127 | |
| 128 | /// Forgets picked drawings the page no longer holds, after a change made elsewhere. A |
| 129 | /// stroke, shape or lasso under way goes on; a drag of drawings ends, and the eraser's |
| 130 | /// next touch starts an undo step of its own. |
| 131 | pub(super) fn refresh_ink(&mut self) { |
| 132 | match &mut self.ink.gesture { |
| 133 | Some(Gesture::Move { .. }) => self.ink.gesture = None, |
| 134 | Some(Gesture::Erase { erased, .. }) => *erased = false, |
| 135 | _ => {} |
| 136 | } |
| 137 | let editor = &self.editor; |
| 138 | self.ink |
| 139 | .selection |
| 140 | .retain(|id| editor.ink_extent(std::slice::from_ref(id)).is_some()); |
| 141 | } |
| 142 | |
| 143 | fn reach(&self) -> f32 { |
| 144 | ERASER_REACH * self.pixel() |
| 145 | } |
| 146 | |
| 147 | /// A press at page point `point`, when a drawing tool takes it or it picks ink. |
| 148 | pub(super) fn ink_pressed(&mut self, point: [f32; 2]) -> Result<Option<Response>> { |
| 149 | let selected = !self.ink.selection.is_empty() |
| 150 | && self |
| 151 | .editor |
| 152 | .ink_extent(&self.ink.selection) |
| 153 | .is_some_and(|[x0, y0, x1, y1]| { |
| 154 | (x0..=x1).contains(&point[0]) && (y0..=y1).contains(&point[1]) |
| 155 | }); |
| 156 | let gesture = match self.ink.tool { |
| 157 | Tool::Select | Tool::Lasso if selected => Gesture::Move { press: point }, |
| 158 | Tool::Select => { |
| 159 | self.ink.selection.clear(); |
| 160 | if self.hit_test(point).is_some() { |
| 161 | return Ok(None); |
| 162 | } |
| 163 | let Some(id) = self.editor.ink_at(point, self.reach()) else { |
| 164 | return Ok(None); |
| 165 | }; |
| 166 | self.set_object_focus(None); |
| 167 | self.ink.selection = vec![id]; |
| 168 | Gesture::Move { press: point } |
| 169 | } |
| 170 | Tool::Pen(_) => Gesture::Stroke(vec![point], self.ink.pressure.into_iter().collect()), |
| 171 | Tool::Shape(..) => Gesture::Shape(self.grid(point)), |
| 172 | Tool::Eraser => { |
| 173 | let mark = self.editor.pending_ops(); |
| 174 | let erased = self.editor.erase(point, point, self.reach(), false)?; |
| 175 | Gesture::Erase { |
| 176 | last: point, |
| 177 | erased, |
| 178 | mark, |
| 179 | } |
| 180 | } |
| 181 | Tool::Lasso => { |
| 182 | self.ink.selection.clear(); |
| 183 | Gesture::Lasso(vec![point]) |
| 184 | } |
| 185 | }; |
| 186 | self.drag = None; |
| 187 | self.ink.gesture = Some(gesture); |
| 188 | Ok(Some(self.changed()?)) |
| 189 | } |
| 190 | |
| 191 | /// The pointer moved to page point `point` during a gesture. |
| 192 | pub(super) fn ink_moved(&mut self, point: [f32; 2]) -> Result<Response> { |
| 193 | let pressure = self.ink.pressure; |
| 194 | match &mut self.ink.gesture { |
| 195 | Some(Gesture::Stroke(points, _) | Gesture::Lasso(points)) => { |
| 196 | let last = points[points.len() - 1]; |
| 197 | // Samples closer than a HIMETRIC unit store as one point. |
| 198 | if (point[0] - last[0]).hypot(point[1] - last[1]) >= 72.0 / 2540.0 { |
| 199 | points.push(point); |
| 200 | if let Some(Gesture::Stroke(_, levels)) = &mut self.ink.gesture |
| 201 | && let Some(&last) = levels.last() |
| 202 | { |
| 203 | levels.push(pressure.unwrap_or(last)); |
| 204 | } |
| 205 | } |
| 206 | Ok(Response::redraw()) |
| 207 | } |
| 208 | Some(Gesture::Erase { last, erased, .. }) => { |
| 209 | let from = std::mem::replace(last, point); |
| 210 | let join = *erased; |
| 211 | if self.editor.erase(from, point, self.reach(), join)? { |
| 212 | if let Some(Gesture::Erase { erased, .. }) = &mut self.ink.gesture { |
| 213 | *erased = true; |
| 214 | } |
| 215 | return self.changed(); |
| 216 | } |
| 217 | Ok(Response::default()) |
| 218 | } |
| 219 | Some(Gesture::Move { press }) => { |
| 220 | let delta = [point[0] - press[0], point[1] - press[1]]; |
| 221 | self.editor |
| 222 | .drag_ink(Some((self.ink.selection.clone(), delta))); |
| 223 | Ok(Response::redraw()) |
| 224 | } |
| 225 | Some(Gesture::Shape(_)) => Ok(Response::redraw()), |
| 226 | None => Ok(Response::default()), |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | /// The press ended at page point `point`, finishing the gesture. |
| 231 | pub(super) fn ink_released(&mut self, point: [f32; 2]) -> Result<Response> { |
| 232 | let Some(gesture) = self.ink.gesture.take() else { |
| 233 | return Ok(Response::default()); |
| 234 | }; |
| 235 | match gesture { |
| 236 | Gesture::Stroke(points, pressure) => { |
| 237 | let Tool::Pen(pen) = self.ink.tool else { |
| 238 | return Ok(Response::default()); |
| 239 | }; |
| 240 | self.editor.draw(Ink { |
| 241 | id: new_id()?, |
| 242 | layout: Default::default(), |
| 243 | strokes: vec![pen.stroke(&points, &pressure)?], |
| 244 | groups: Vec::new(), |
| 245 | shape: None, |
| 246 | })?; |
| 247 | } |
| 248 | Gesture::Shape(from) => { |
| 249 | let Tool::Shape(kind, pen) = self.ink.tool else { |
| 250 | return Ok(Response::default()); |
| 251 | }; |
| 252 | let to = self.grid(point); |
| 253 | if from == to { |
| 254 | return Ok(Response::redraw()); |
| 255 | } |
| 256 | self.editor |
| 257 | .draw(Ink::drawn(kind, from, to, &pen.stroke(&[], &[])?)?)?; |
| 258 | } |
| 259 | Gesture::Erase { .. } => return Ok(Response::redraw()), |
| 260 | Gesture::Lasso(mut points) => { |
| 261 | points.push(point); |
| 262 | self.ink.selection = self.editor.ink_within(&points); |
| 263 | return Ok(Response::redraw()); |
| 264 | } |
| 265 | Gesture::Move { press } => { |
| 266 | let delta = [point[0] - press[0], point[1] - press[1]]; |
| 267 | // As OneNote does, a drawing dropped outside the view stays where it was. |
| 268 | let inside = (0..2).all(|axis| { |
| 269 | (0.0..self.viewport.size[axis] as f32).contains(&self.pointer[axis]) |
| 270 | }); |
| 271 | let selection = self.ink.selection.clone(); |
| 272 | self.editor |
| 273 | .move_ink(&selection, if inside { delta } else { [0.0; 2] })?; |
| 274 | } |
| 275 | } |
| 276 | self.changed() |
| 277 | } |
| 278 | |
| 279 | /// Delete and Backspace delete picked drawings; Escape lets them go, then leaves a |
| 280 | /// drawing tool for Select & Type, as OneNote's Escape does. |
| 281 | pub(super) fn ink_key(&mut self, key: &Key) -> Result<Option<Response>> { |
| 282 | match key { |
| 283 | Key::Named(NamedKey::Backspace | NamedKey::Delete) |
| 284 | if !self.ink.selection.is_empty() => |
| 285 | { |
| 286 | let selection = std::mem::take(&mut self.ink.selection); |
| 287 | self.editor.delete_ink(&selection)?; |
| 288 | Ok(Some(self.changed()?)) |
| 289 | } |
| 290 | Key::Named(NamedKey::Escape) if !self.ink.selection.is_empty() => { |
| 291 | self.ink.selection.clear(); |
| 292 | Ok(Some(Response::redraw())) |
| 293 | } |
| 294 | Key::Named(NamedKey::Escape) if self.ink.tool != Tool::Select => { |
| 295 | Ok(Some(self.set_tool(Tool::Select))) |
| 296 | } |
| 297 | _ => { |
| 298 | self.ink.selection.clear(); |
| 299 | Ok(None) |
| 300 | } |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | /// Whether a press on the page began a gesture of the drawing tools. |
| 305 | pub fn inking(&self) -> bool { |
| 306 | self.ink.gesture.is_some() |
| 307 | } |
| 308 | |
| 309 | /// Whether a drawing tool takes presses, which then show a crosshair. |
| 310 | pub(super) fn drawing(&self) -> bool { |
| 311 | self.ink.tool != Tool::Select |
| 312 | } |
| 313 | |
| 314 | /// The gesture under way and the picked drawings' frame, over the page. |
| 315 | pub(super) fn append_ink_feedback( |
| 316 | &self, |
| 317 | automatic: [f32; 4], |
| 318 | primitives: &mut Vec<Primitive<'_>>, |
| 319 | ) { |
| 320 | let pixel = self.pixel(); |
| 321 | let point = self.viewport.document_point(self.pointer); |
| 322 | let preview = |ink: Ink, primitives: &mut Vec<Primitive<'_>>| { |
| 323 | crate::gpu::page::append_ink(&ink, [0.0; 2], automatic, primitives) |
| 324 | }; |
| 325 | match (&self.ink.gesture, self.ink.tool) { |
| 326 | (Some(Gesture::Stroke(points, pressure)), Tool::Pen(pen)) => { |
| 327 | if let Ok(stroke) = pen.stroke(points, pressure) { |
| 328 | preview(ink(vec![stroke]), primitives); |
| 329 | } |
| 330 | } |
| 331 | (Some(Gesture::Shape(from)), Tool::Shape(kind, pen)) => { |
| 332 | let to = self.grid(point); |
| 333 | if let Ok(pen) = pen.stroke(&[], &[]) |
| 334 | && let Ok(shape) = Ink::drawn(kind, *from, to, &pen) |
| 335 | { |
| 336 | preview(shape, primitives); |
| 337 | } |
| 338 | } |
| 339 | (Some(Gesture::Lasso(points)), _) => { |
| 340 | let lasso = colorref(0x00808080); |
| 341 | for pair in points.windows(2) { |
| 342 | primitives.push(Primitive::Segment { |
| 343 | from: pair[0], |
| 344 | to: pair[1], |
| 345 | width: pixel, |
| 346 | round: true, |
| 347 | color: lasso, |
| 348 | }); |
| 349 | } |
| 350 | } |
| 351 | _ => {} |
| 352 | } |
| 353 | if let Some([x0, y0, x1, y1]) = self.editor.ink_extent(&self.ink.selection) { |
| 354 | let pad = 3.0 * pixel; |
| 355 | primitives.push(Primitive::RoundedRect { |
| 356 | rect: [x0 - pad, y0 - pad, x1 + pad, y1 + pad], |
| 357 | radius: [0.0; 2], |
| 358 | stroke: Some(Stroke::Dashed(pixel)), |
| 359 | color: colorref(0x00e0a060), |
| 360 | }); |
| 361 | } |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | fn ink(strokes: Vec<InkStroke>) -> Ink { |
| 366 | Ink { |
| 367 | id: ExGuid::default(), |
| 368 | layout: Default::default(), |
| 369 | strokes, |
| 370 | groups: Vec::new(), |
| 371 | shape: None, |
| 372 | } |
| 373 | } |