1use super::ink_stencil_input::InkStencilInput;
2use super::*;
3use ferroui_controls::{Control, ControlImpl};
4use ferroui_base::input::{
5 IPointer, InputElement, InputElementImpl, InputElementImplExt, PointerCaptureLostEventArgs,
6 PointerEventArgs, PointerPressedEventArgs, PointerReleasedEventArgs,
7};
8use ferroui_base::interactivity::InteractiveImpl;
9use ferroui_base::layout::LayoutableImpl;
10use ferroui_base::media::immutable::{ImmutablePen, ImmutableSolidColorBrush};
11use ferroui_base::media::{
12 Color, DrawingContext, FillRule, FlowDirection, FormattedText, Geometry, IBrush, IPen,
13 StreamGeometry, Typeface,
14};
15use ferroui_base::platform::IGeometryContext;
16use ferroui_base::reactive::IDisposable;
17use ferroui_base::utilities::CultureInfo;
18use ferroui_base::{
19 ferro_class, ferro_impl_classes, instantiate, FerroObjectImpl, FerroObjectImplExt,
20 FerroPropertyChangedEventArgs, Point, Rect, Ref, Size, StyledElementImpl, VisualImpl,
21 VisualImplExt, VisualTreeAttachmentEventArgs,
22};
23use std::cell::RefCell;
24use std::collections::{BTreeMap, BTreeSet};
25use std::rc::Rc;
26
27struct CachedGeometry {
28 polygons: Rc<Vec<Vec<Point>>>,
29 geometry: Ref<Geometry>,
30}
31
32struct CachedStencilLabel {
33 tenths: u64,
34 color: Color,
35 text: Rc<FormattedText>,
36}
37
38#[repr(C)]
40pub struct InkCanvas {
41 base: Control,
42 presenter: Rc<InkPresenter>,
43 geometries: RefCell<BTreeMap<u32, CachedGeometry>>,
44 captured: RefCell<BTreeMap<i32, Rc<dyn IPointer>>>,
45 stencil_input: RefCell<InkStencilInput>,
46 stencil_labels: RefCell<[Option<CachedStencilLabel>; 2]>,
47 color_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
48}
49
50ferro_class!(InkCanvas: Control);
51ferroui_base::ferro_class_info!(InkCanvas {
52 new: InkCanvas::new,
53 markup: { properties: [InkPresenter: Rc<InkPresenter> { get: InkCanvas::ink_presenter }] },
54});
55ferro_impl_classes!(InkCanvas: StyledElementImpl, InteractiveImpl);
56
57impl FerroObjectImpl for InkCanvas {
58 fn constructed(this: &Self) {
59 Self::parent_constructed(this);
60 let weak = this.to_ref().downgrade();
61 this.presenter.changed(move || {
62 if let Some(canvas) = weak.upgrade() {
63 canvas
64 .stencil_input
65 .borrow_mut()
66 .reconcile(&canvas.presenter);
67 canvas.release_inactive_pointers();
68 canvas.invalidate_visual();
69 }
70 });
71 }
72 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
73 Self::parent_on_property_changed(this, change);
74 if change.property() == InputElement::is_effectively_enabled_property().as_property()
75 && !this.is_effectively_enabled()
76 {
77 this.stencil_input.borrow_mut().clear();
78 this.presenter.cancel_all();
79 }
80 }
81}
82
83impl ControlImpl for InkCanvas {
84 fn on_create_automation_peer(this: &Self) -> Ref<ferroui_controls::automation::peers::AutomationPeer> {
85 InkCanvasAutomationPeer::new(this).upcast()
86 }
87}
88
89impl LayoutableImpl for InkCanvas {
90 fn measure_override(_this: &Self, _available_size: Size) -> Size {
91 Size::default()
92 }
93 fn arrange_override(this: &Self, final_size: Size) -> Size {
94 this.presenter.ruler().initialize_position(final_size);
95 this.presenter.protractor().initialize_position(final_size);
96 this.presenter.set_size(final_size);
97 final_size
98 }
99}
100
101impl VisualImpl for InkCanvas {
102 fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
103 Self::parent_on_attached_to_visual_tree(this, e);
104 if let Some(settings) = this
105 .presentation_source()
106 .and_then(|source| source.platform_settings())
107 {
108 let weak = this.to_ref().downgrade();
109 let subscription = settings.color_values_changed(Rc::new(move |_| {
110 if let Some(canvas) = weak.upgrade() {
111 canvas.invalidate_visual();
112 }
113 }));
114 let old = this.color_subscription.replace(Some(subscription));
115 if let Some(old) = old {
116 old.dispose();
117 }
118 }
119 }
120 fn render(this: &Self, context: &mut DrawingContext) {
121 let transparent: Rc<dyn IBrush> = Rc::new(ImmutableSolidColorBrush::from_uint32(0));
123 let bounds = Rect::from_size(this.bounds().size());
124 context.draw_rectangle(
125 Some(&transparent),
126 None,
127 bounds,
128 0.0,
129 0.0,
130 &Default::default(),
131 );
132 let clip = context.push_clip(bounds);
133 let strokes = this.presenter.visible_strokes();
134 let ids: BTreeSet<_> = strokes.iter().map(|s| s.id()).collect();
135 this.geometries
136 .borrow_mut()
137 .retain(|id, _| ids.contains(id));
138 for stroke in &strokes {
139 this.render_stroke(context, stroke);
140 }
141 this.render_stencils(context);
142 context.pop(clip);
143 }
144 fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
145 this.stencil_input.borrow_mut().clear();
146 this.presenter.cancel_all();
147 let subscription = this.color_subscription.borrow_mut().take();
148 if let Some(subscription) = subscription {
149 subscription.dispose();
150 }
151 this.geometries.borrow_mut().clear();
152 Self::parent_on_detached_from_visual_tree(this, e);
153 }
154}
155
156impl InputElementImpl for InkCanvas {
157 fn on_pointer_pressed(this: &Self, e: &PointerPressedEventArgs) {
158 Self::parent_on_pointer_pressed(this, e);
159 if e.handled() || !this.is_effectively_enabled() {
160 return;
161 }
162 let point = e.get_current_point(Some(this));
163 let stencil = this.stencil_input.borrow_mut().press(
164 &this.presenter,
165 e.pointer().id(),
166 e.pointer().type_(),
167 point.position,
168 point.properties,
169 e.key_modifiers(),
170 );
171 if stencil {
172 this.captured
173 .borrow_mut()
174 .insert(e.pointer().id(), e.pointer().clone());
175 e.pointer()
176 .capture(Some(&this.to_ref().upcast::<InputElement>()));
177 e.prevent_gesture_recognition();
178 e.set_handled(true);
179 this.invalidate_visual();
180 return;
181 }
182 if this.feed(InkPointerPhase::Pressed, e) {
183 if this.presenter.has_active_pointer(e.pointer().id()) {
184 this.captured
185 .borrow_mut()
186 .insert(e.pointer().id(), e.pointer().clone());
187 e.pointer()
188 .capture(Some(&this.to_ref().upcast::<InputElement>()));
189 }
190 e.prevent_gesture_recognition();
191 e.set_handled(true);
192 }
193 }
194 fn on_pointer_moved(this: &Self, e: &PointerEventArgs) {
195 Self::parent_on_pointer_moved(this, e);
196 if !this.is_effectively_enabled() {
197 return;
198 }
199 if this.stencil_input.borrow().contains(e.pointer().id()) {
200 this.move_stencil(e);
201 e.prevent_gesture_recognition();
202 e.set_handled(true);
203 return;
204 }
205 let mut handled = false;
206 for point in e.get_intermediate_points(Some(this)) {
207 let args = Self::sample(e, point.position, point.properties);
208 match this
209 .presenter
210 .process_pointer(InkPointerPhase::Moved, &args)
211 {
212 Ok(value) => handled |= value,
213 Err(_) => {
214 this.presenter.cancel_pointer(e.pointer().id());
215 break;
216 }
217 }
218 }
219 if handled {
220 e.prevent_gesture_recognition();
221 e.set_handled(true);
222 }
223 }
224 fn on_pointer_released(this: &Self, e: &PointerReleasedEventArgs) {
225 Self::parent_on_pointer_released(this, e);
226 if this.stencil_input.borrow().contains(e.pointer().id()) {
227 this.move_stencil(e);
228 this.stencil_input
229 .borrow_mut()
230 .release(&this.presenter, e.pointer().id());
231 this.release_inactive_pointers();
232 this.invalidate_visual();
233 e.prevent_gesture_recognition();
234 e.set_handled(true);
235 return;
236 }
237 if this.feed(InkPointerPhase::Released, e) {
238 e.set_handled(true);
239 }
240 if e.pointer()
241 .captured()
242 .is_some_and(|v| v == this.to_ref().upcast::<InputElement>())
243 {
244 e.pointer().capture(None);
245 }
246 }
247 fn on_pointer_entered(this: &Self, e: &PointerEventArgs) {
248 Self::parent_on_pointer_entered(this, e);
249 if this.feed(InkPointerPhase::Entered, e) {
250 e.set_handled(true);
251 }
252 }
253 fn on_pointer_exited(this: &Self, e: &PointerEventArgs) {
254 Self::parent_on_pointer_exited(this, e);
255 if this.feed(InkPointerPhase::Exited, e) {
256 e.set_handled(true);
257 }
258 }
259 fn on_pointer_capture_lost(this: &Self, e: &PointerCaptureLostEventArgs) {
260 this.captured.borrow_mut().remove(&e.pointer().id());
261 this.stencil_input
262 .borrow_mut()
263 .release(&this.presenter, e.pointer().id());
264 this.presenter.cancel_pointer(e.pointer().id());
265 this.invalidate_visual();
266 Self::parent_on_pointer_capture_lost(this, e);
267 }
268}
269
270impl InkCanvas {
271 pub fn construct() -> Self {
272 Self {
273 base: Control::construct(),
274 presenter: InkPresenter::new(),
275 geometries: RefCell::new(BTreeMap::new()),
276 captured: RefCell::new(BTreeMap::new()),
277 stencil_input: RefCell::new(InkStencilInput::default()),
278 stencil_labels: RefCell::new([None, None]),
279 color_subscription: RefCell::new(None),
280 }
281 }
282 pub fn new() -> Ref<Self> {
283 instantiate(Self::construct())
284 }
285 pub fn ink_presenter(&self) -> Rc<InkPresenter> {
286 self.presenter.clone()
287 }
288
289 fn release_inactive_pointers(&self) {
290 let stale: Vec<_> = self
291 .captured
292 .borrow()
293 .iter()
294 .filter(|(id, _)| {
295 !self.presenter.has_active_pointer(**id)
296 && !self.stencil_input.borrow().contains(**id)
297 })
298 .map(|(id, pointer)| (*id, pointer.clone()))
299 .collect();
300 {
301 let mut captured = self.captured.borrow_mut();
302 for (id, _) in &stale {
303 captured.remove(id);
304 }
305 }
306 for (_, pointer) in stale {
307 if pointer
308 .captured()
309 .is_some_and(|owner| owner == self.to_ref().upcast::<InputElement>())
310 {
311 pointer.capture(None);
312 }
313 }
314 }
315
316 fn move_stencil(&self, event: &PointerEventArgs) {
317 let update = self.stencil_input.borrow_mut().moved(
318 &self.presenter,
319 event.pointer().id(),
320 event.get_position(Some(self)),
321 );
322 if let Some(update) = update {
323 if update.apply(&self.presenter).is_err() {
324 self.stencil_input
325 .borrow_mut()
326 .release(&self.presenter, event.pointer().id());
327 }
328 }
329 self.release_inactive_pointers();
330 self.invalidate_visual();
331 }
332
333 fn sample(
334 e: &PointerEventArgs,
335 position: Point,
336 properties: ferroui_base::input::PointerPointProperties,
337 ) -> InkPointerEventArgs {
338 let mut sample = InkPointerEventArgs::new(
339 e.pointer().id(),
340 e.pointer().type_(),
341 InkPoint {
342 position,
343 pressure: properties.pressure,
344 tilt_x: properties.x_tilt,
345 tilt_y: properties.y_tilt,
346 twist: properties.twist,
349 timestamp: e.timestamp().saturating_mul(1000),
350 },
351 );
352 sample.is_eraser = properties.is_eraser || properties.is_inverted;
353 sample.is_barrel_button_pressed = properties.is_barrel_button_pressed;
354 sample.is_right_button_pressed = properties.is_right_button_pressed;
355 sample
356 }
357 fn feed(&self, phase: InkPointerPhase, e: &PointerEventArgs) -> bool {
358 let point = e.get_current_point(Some(self));
359 let sample = Self::sample(e, point.position, point.properties);
360 match self.presenter.process_pointer(phase, &sample) {
361 Ok(handled) => handled,
362 Err(_) => {
363 self.presenter.cancel_pointer(e.pointer().id());
364 false
365 }
366 }
367 }
368 fn render_stroke(&self, context: &mut DrawingContext, stroke: &Rc<InkStroke>) {
369 let polygons = stroke.outline_polygons();
370 let cached = self
371 .geometries
372 .borrow()
373 .get(&stroke.id())
374 .filter(|cached| Rc::ptr_eq(&cached.polygons, &polygons))
375 .map(|cached| cached.geometry.clone());
376 let geometry = cached.unwrap_or_else(|| {
377 let geometry = Self::create_geometry(&polygons);
378 self.geometries.borrow_mut().insert(
379 stroke.id(),
380 CachedGeometry {
381 polygons,
382 geometry: geometry.clone(),
383 },
384 );
385 geometry
386 });
387 let attributes = stroke.drawing_attributes();
388 let mut color = attributes.color;
389 if let Some((background, system)) =
390 super::color_names::high_contrast_colors(self, attributes.draw_as_highlighter)
391 {
392 color = super::color_names::adjust_high_contrast_color(
393 color,
394 background,
395 system,
396 self.presenter.high_contrast_adjustment(),
397 );
398 }
399 let brush: Rc<dyn IBrush> = Rc::new(ImmutableSolidColorBrush::with_opacity(
402 color,
403 (if attributes.draw_as_highlighter {
404 0.5
405 } else {
406 1.0
407 }) * (if attributes.kind == InkDrawingAttributesKind::Pencil {
408 f64::from(attributes.pencil_opacity)
409 } else {
410 1.0
411 }),
412 ));
413 context.draw_geometry(Some(&brush), None, &geometry);
414 }
415 fn create_geometry(polygons: &[Vec<Point>]) -> Ref<Geometry> {
416 let geometry = StreamGeometry::new();
417 let mut builder = geometry.open();
418 builder.set_fill_rule(FillRule::NonZero);
419 for polygon in polygons {
420 if let Some(first) = polygon.first() {
421 builder.begin_figure(*first, true);
422 for point in &polygon[1..] {
423 builder.line_to(*point, false);
424 }
425 builder.end_figure(true);
426 }
427 }
428 builder.dispose();
429 geometry.upcast()
430 }
431 fn render_stencils(&self, context: &mut DrawingContext) {
432 let ruler = self.presenter.ruler();
433 if ruler.is_visible() {
434 let fill: Rc<dyn IBrush> =
435 Rc::new(ImmutableSolidColorBrush::new(ruler.background_color()));
436 let foreground = Rc::new(ImmutableSolidColorBrush::new(ruler.foreground_color()));
437 let scale = Self::stencil_scale(ruler.transform());
438 let pen: Rc<dyn IPen> =
439 Rc::new(ImmutablePen::with_brush(Some(foreground), 1.0 / scale));
440 let opacity =
441 context.push_opacity(self.stencil_opacity(InkPresenterStencilKind::Ruler));
442 let state = context.push_transform(ruler.transform());
443 context.draw_rectangle(
444 Some(&fill),
445 Some(&pen),
446 Rect::new(0.0, 0.0, ruler.length(), ruler.width()),
447 0.0,
448 0.0,
449 &Default::default(),
450 );
451 if ruler.are_tick_marks_visible() {
452 for index in 0..=(ruler.length() / 10.0) as usize {
453 let x = index as f64 * 10.0;
454 let scale_y = ruler
455 .transform()
456 .m21
457 .hypot(ruler.transform().m22)
458 .max(1.0e-3);
459 let length = ((if index % 5 == 0 { 12.0_f64 } else { 6.0 }) / scale_y)
460 .min(ruler.width() * 0.3);
461 context.draw_line(&pen, Point::new(x, 0.0), Point::new(x, length));
462 context.draw_line(
463 &pen,
464 Point::new(x, ruler.width()),
465 Point::new(x, ruler.width() - length),
466 );
467 }
468 }
469 context.pop(state);
470 if ruler.is_compass_visible() {
471 let label = self.stencil_label(0, ruler.angle(), ruler.foreground_color());
472 let center = ruler.center() * ruler.transform();
473 context.draw_text(
474 &label,
475 Point::new(
476 center.x - label.width() * 0.5,
477 center.y - label.height() * 0.5,
478 ),
479 );
480 }
481 context.pop(opacity);
482 }
483 let protractor = self.presenter.protractor();
484 if protractor.is_visible() {
485 let fill: Rc<dyn IBrush> =
486 Rc::new(ImmutableSolidColorBrush::new(protractor.background_color()));
487 let foreground = Rc::new(ImmutableSolidColorBrush::new(protractor.foreground_color()));
488 let accent = Rc::new(ImmutableSolidColorBrush::new(protractor.accent_color()));
489 let scale = Self::stencil_scale(protractor.transform());
490 let pen: Rc<dyn IPen> =
491 Rc::new(ImmutablePen::with_brush(Some(foreground), 1.0 / scale));
492 let accent_pen: Rc<dyn IPen> =
493 Rc::new(ImmutablePen::with_brush(Some(accent), 1.5 / scale));
494 let opacity =
495 context.push_opacity(self.stencil_opacity(InkPresenterStencilKind::Protractor));
496 let state = context.push_transform(protractor.transform());
497 let radius = protractor.radius();
498 let center = protractor.center();
499 context.draw_ellipse(
500 Some(&fill),
501 Some(&pen),
502 Rect::new(0.0, 0.0, radius * 2.0, radius * 2.0),
503 );
504 let radial_point = |angle: f64, distance: f64| {
505 Point::new(
506 center.x + angle.cos() * distance,
507 center.y + angle.sin() * distance,
508 )
509 };
510 if protractor.are_tick_marks_visible() {
511 for tick in 0..72 {
512 let angle = f64::from(tick * 5).to_radians();
513 let length =
514 ((if tick % 3 == 0 { 12.0_f64 } else { 6.0 }) / scale).min(radius * 0.2);
515 context.draw_line(
516 &pen,
517 radial_point(angle, radius - length),
518 radial_point(angle, radius),
519 );
520 }
521 }
522 let mark_length = (18.0 / scale).min(radius * 0.3);
523 for angle in [0.0, std::f64::consts::PI] {
524 context.draw_line(
525 &accent_pen,
526 radial_point(angle, radius - mark_length),
527 radial_point(angle, radius),
528 );
529 }
530 if protractor.is_center_marker_visible() {
531 let half = (8.0 / scale).min(radius * 0.2);
532 context.draw_line(
533 &pen,
534 Point::new(center.x - half, center.y),
535 Point::new(center.x + half, center.y),
536 );
537 context.draw_line(
538 &pen,
539 Point::new(center.x, center.y - half),
540 Point::new(center.x, center.y + half),
541 );
542 }
543 if protractor.are_rays_visible() {
544 if let Some((start, end)) = protractor.rays() {
545 context.draw_line(&accent_pen, center, radial_point(start, radius));
546 context.draw_line(&accent_pen, center, radial_point(end, radius));
547 }
548 }
549 context.pop(state);
550 if protractor.is_angle_readout_visible() {
551 if let Some(angle) = protractor.angle() {
552 let label = self.stencil_label(1, angle, protractor.foreground_color());
553 let center = center * protractor.transform();
554 context.draw_text(
555 &label,
556 Point::new(center.x - label.width() * 0.5, center.y + 12.0),
557 );
558 }
559 }
560 context.pop(opacity);
561 }
562 }
563
564 fn stencil_scale(transform: ferroui_base::Matrix) -> f64 {
565 ((transform.m11.hypot(transform.m12) + transform.m21.hypot(transform.m22)) * 0.5)
566 .max(1.0e-3)
567 }
568
569 fn stencil_opacity(&self, kind: InkPresenterStencilKind) -> f64 {
570 if self.stencil_input.borrow().is_manipulating(kind) {
571 0.5
572 } else {
573 0.75
574 }
575 }
576
577 fn stencil_label(&self, index: usize, angle: f64, color: Color) -> Rc<FormattedText> {
578 let tenths = (angle * 10.0).round() as u64;
579 let cached = self.stencil_labels.borrow()[index]
580 .as_ref()
581 .filter(|v| v.tenths == tenths && v.color == color)
582 .map(|v| v.text.clone());
583 if let Some(cached) = cached {
584 return cached;
585 }
586 let text = Rc::new(FormattedText::new(
587 &format!("{}.{:01}°", tenths / 10, tenths % 10),
588 CultureInfo::current_culture(),
589 FlowDirection::LeftToRight,
590 Typeface::default_typeface(),
591 14.0,
592 Some(Rc::new(ImmutableSolidColorBrush::new(color))),
593 ));
594 self.stencil_labels.borrow_mut()[index] = Some(CachedStencilLabel {
595 tenths,
596 color,
597 text: text.clone(),
598 });
599 text
600 }
601}